diff --git a/docs/en_US/dbms_job.rst b/docs/en_US/dbms_job.rst new file mode 100644 index 000000000..8254e03a7 --- /dev/null +++ b/docs/en_US/dbms_job.rst @@ -0,0 +1,108 @@ +.. _dbms_job: + +***************** +`DBMS Job`:index: +***************** + +Use the *DBMS Job* dialog to create a DBMS Job. + +.. image:: images/dbms_job_general.png + :alt: DBMS Job dialog general tab + :align: center + +Use the fields in the *General* tab to create job: + +* Use the *Name* field to add a descriptive name for the job. The name will + be displayed in the *pgAdmin* object explorer. +* Use the *Enabled?* switch to indicate that job should be enabled or disabled. +* Use the *Job Type* field to select the type of the job. Type could be SELF-CONTAINED or PRE-DEFINED. + If the Job Type is Self-Contained you need to specify the action and repeat interval in the Action and Repeat tabs respectively. + If the Job Type is Pre-Defined you need to specify the existing Program and Schedule names in the Pre-Defined tab. +* Store notes about the job in the *Comment* field. + +Click the *Action* tab to continue. + +.. image:: images/dbms_job_action.png + :alt: DBMS Job dialog action tab + :align: center + +Use the *Action* tab to select the action for the job. This tab is only enabled when the job type is 'SELF-CONTAINED'. + +* Use the *Type* field to select the type of the job. Type could be PLSQL BLOCK or STORED PROCEDURE. +* Use the *Procedure* field to select an existing procedure that executes when the job is invoked. +* *Number of Arguments* field is read-only and indicates the quantity of arguments necessary for the chosen procedure. + +Click the *Code* tab to continue. + +.. image:: images/dbms_job_code.png + :alt: DBMS Job dialog code tab + :align: center + +* Use the *Code* field to write the code that executes when the job is invoked. + This tab is only enabled when the job type is 'SELF-CONTAINED' and type of the action is set to 'PLSQL BLOCK'. + + +Click the *Arguments* tab to continue. + +.. image:: images/dbms_job_arguments.png + :alt: DBMS Job dialog arguments tab + :align: center + +* *Arguments* tab outlines the arguments required by the selected procedure in the 'Action' tab. This tab is only enabled when the job type is 'SELF-CONTAINED'. + + +Click the *Repeat* tab to continue. + +.. image:: images/dbms_job_repeat.png + :alt: DBMS Job dialog repeat tab + :align: center + +Use the *Repeat* tab to select the repeat interval for the job. This tab is only enabled when the job type is 'SELF-CONTAINED'. + +* Use the calendar selector in the *Start* field to specify the starting date + and time for the job. +* Use the calendar selector in the *End* field to specify the ending date and + time for the job. +* Use the *Frequency* field to select the frequency. Frequency is one of the following: + YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY. +* Use the *Date* field to select the date on which job will execute.Date is YYYYMMDD. +* Use the *Months* field to select the months in which the job will execute. +* Use the *Week Days* field to select the days on which the job will execute. +* Use the *Month Days* field to select the numeric days on which the job will + execute. +* Use the *Hours* field to select the hour at which the job will execute. +* Use the *Minutes* field to select the minute at which the job will execute. + +Click the *Pre-Defined* tab to continue. + +.. image:: images/dbms_job_predefined.png + :alt: DBMS Job dialog predefined tab + :align: center + +Use the *Pre-Defined* tab to select the existing program and schedule to create the job. +This tab is only enabled when the job type is 'PRE-DEFINED'. + +* Use the *Program Name* field to select the existing program. +* Use the *Schedule Name* field to select an existing schedule. + + +Click the *SQL* tab to continue. + +Your entries in the *DBMS Job* dialog generate a SQL command (see an example below). +Use the *SQL* tab for review; revisit or switch tabs to make any changes to the +SQL command. + +**Example** + +The following is an example of the sql command generated by user selections in +the *DBMS Job* dialog: + +.. image:: images/dbms_job_sql.png + :alt: DBMS Job dialog sql tab + :align: center + +* Click the *Info* button (i) to access online help. +* Click the *Help* button (?) to access dialog help. +* Click the *Save* button to save work. +* Click the *Close* button to exit without saving work. +* Click the *Reset* button to restore configuration parameters. \ No newline at end of file diff --git a/docs/en_US/dbms_job_scheduler.rst b/docs/en_US/dbms_job_scheduler.rst new file mode 100644 index 000000000..b5022fb7c --- /dev/null +++ b/docs/en_US/dbms_job_scheduler.rst @@ -0,0 +1,45 @@ +.. _dbms_job_scheduler: + +******************************** +`Using EDB Job Scheduler`:index: +******************************** + +In the past versions of EPAS, DBMS_SCHEDULER or DBMS_JOBS required the configuration +of pgAgent, an essential service for their functionality. Maintaining pgAgent in a +production environment is cumbersome. The need for correct configuration, regular updates, +and ensuring the service’s health added complexity. + +EPAS 16 revolutionizes job scheduling by eliminating the need for the pgAgent component. +The new version introduces EDB Job Scheduler which is an extension that runs the job scheduler +as a background process for the DBMS_SCHEDULER and DBMS_JOB packages. + +The EDB Job Scheduler has a scheduler process that starts when the database cluster starts. +To start the scheduler process, load the EDB Job Scheduler extension using the **shared_preload_libraries** +parameter. After you load the extension, create the extension using the CREATE EXTENSION command. +The database in which you're creating the extension must be listed in the **edb_job_scheduler.database_list** +parameter. + +Instructions for configuring the EDB Job Scheduler can be found in the +`Configuring EDB Job Scheduler `_. + +.. image:: images/dbms_job_scheduler.png + :alt: DBMS Job Scheduler Object Browser + :align: center + +Check the status of all the jobs +******************************** + +To check the running status of all the jobs select the 'DBMS Job Scheduler' collection node from the object +explorer and select the Properties tab. + +.. image:: images/dbms_job_details.png + :alt: DBMS Job Details + :align: center + + +.. toctree:: + :maxdepth: 1 + + dbms_job + dbms_program + dbms_schedule \ No newline at end of file diff --git a/docs/en_US/dbms_program.rst b/docs/en_US/dbms_program.rst new file mode 100644 index 000000000..0c0aa85f4 --- /dev/null +++ b/docs/en_US/dbms_program.rst @@ -0,0 +1,71 @@ +.. _dbms_program: + +********************* +`DBMS Program`:index: +********************* + +Use the *DBMS Program* dialog to create a DBMS Program. + +.. image:: images/dbms_program_general.png + :alt: DBMS Program dialog general tab + :align: center + +Use the fields in the *General* tab to create program: + +* Use the *Name* field to add a descriptive name for the program. The name will + be displayed in the *pgAdmin* object explorer. +* Use the *Enabled?* switch to indicate that program should be enabled or disabled. +* Store notes about the program in the *Comment* field. + +Click the *Action* tab to continue. + +.. image:: images/dbms_program_action.png + :alt: DBMS Program dialog action tab + :align: center + +Use the *Action* tab to select the action for the program: + +* Use the *Type* field to select the type of the program. Type could be PLSQL BLOCK or STORED PROCEDURE. +* Use the *Procedure* field to select an existing procedure that executes when the program is invoked. +* *Number of Arguments* field is read-only and indicates the quantity of arguments necessary for the chosen procedure. + +Click the *Code* tab to continue. + +.. image:: images/dbms_program_code.png + :alt: DBMS Program dialog code tab + :align: center + +* Use the *Code* field to write the code that executes when the program is invoked. + This tab is only enabled when the type of the program is set to 'PLSQL BLOCK'. + + +Click the *Arguments* tab to continue. + +.. image:: images/dbms_program_arguments.png + :alt: DBMS Program dialog arguments tab + :align: center + +* *Arguments* tab is a read-only section that outlines the arguments required by the selected procedure in the 'Action' tab. + + +Click the *SQL* tab to continue. + +Your entries in the *DBMS Program* dialog generate a SQL command (see an example below). +Use the *SQL* tab for review; revisit or switch tabs to make any changes to the +SQL command. + +**Example** + +The following is an example of the sql command generated by user selections in +the *DBMS Program* dialog: + +.. image:: images/dbms_program_sql.png + :alt: DBMS Program dialog sql tab + :align: center + +* Click the *Info* button (i) to access online help. +* Click the *Help* button (?) to access dialog help. +* Click the *Save* button to save work. +* Click the *Close* button to exit without saving work. +* Click the *Reset* button to restore configuration parameters. + diff --git a/docs/en_US/dbms_schedule.rst b/docs/en_US/dbms_schedule.rst new file mode 100644 index 000000000..9811267ec --- /dev/null +++ b/docs/en_US/dbms_schedule.rst @@ -0,0 +1,61 @@ +.. _dbms_schedule: + +********************** +`DBMS Schedule`:index: +********************** + +Use the *DBMS Schedule* dialog to create a DBMS Schedule. + +.. image:: images/dbms_schedule_general.png + :alt: DBMS Schedule dialog general tab + :align: center + +Use the fields in the *General* tab to create schedule: + +* Use the *Name* field to add a descriptive name for the schedule. The name will + be displayed in the *pgAdmin* object explorer. +* Store notes about the schedule in the *Comment* field. + +Click the *Repeat* tab to continue. + +.. image:: images/dbms_schedule_repeat.png + :alt: DBMS Schedule dialog repeat tab + :align: center + +Use the *Repeat* tab to select the repeat interval for the schedule: + +* Use the calendar selector in the *Start* field to specify the starting date + and time for the schedule. +* Use the calendar selector in the *End* field to specify the ending date and + time for the schedule. +* Use the *Frequency* field to select the frequency. Frequency is one of the following: + YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY. +* Use the *Date* field to select the date on which schedule will execute.Date is YYYYMMDD. +* Use the *Months* field to select the months in which the schedule will execute. +* Use the *Week Days* field to select the days on which the schedule will execute. +* Use the *Month Days* field to select the numeric days on which the schedule will + execute. +* Use the *Hours* field to select the hour at which the schedule will execute. +* Use the *Minutes* field to select the minute at which the schedule will execute. + + +Click the *SQL* tab to continue. + +Your entries in the *DBMS Schedule* dialog generate a SQL command (see an example below). +Use the *SQL* tab for review; revisit or switch tabs to make any changes to the +SQL command. + +**Example** + +The following is an example of the sql command generated by user selections in +the *DBMS Schedule* dialog: + +.. image:: images/dbms_schedule_sql.png + :alt: DBMS Schedule dialog sql tab + :align: center + +* Click the *Info* button (i) to access online help. +* Click the *Help* button (?) to access dialog help. +* Click the *Save* button to save work. +* Click the *Close* button to exit without saving work. +* Click the *Reset* button to restore configuration parameters. \ No newline at end of file diff --git a/docs/en_US/images/dbms_job_action.png b/docs/en_US/images/dbms_job_action.png new file mode 100644 index 000000000..6e7345eb1 Binary files /dev/null and b/docs/en_US/images/dbms_job_action.png differ diff --git a/docs/en_US/images/dbms_job_arguments.png b/docs/en_US/images/dbms_job_arguments.png new file mode 100644 index 000000000..eabad426f Binary files /dev/null and b/docs/en_US/images/dbms_job_arguments.png differ diff --git a/docs/en_US/images/dbms_job_code.png b/docs/en_US/images/dbms_job_code.png new file mode 100644 index 000000000..fe20dbeec Binary files /dev/null and b/docs/en_US/images/dbms_job_code.png differ diff --git a/docs/en_US/images/dbms_job_details.png b/docs/en_US/images/dbms_job_details.png new file mode 100644 index 000000000..4818ab73a Binary files /dev/null and b/docs/en_US/images/dbms_job_details.png differ diff --git a/docs/en_US/images/dbms_job_general.png b/docs/en_US/images/dbms_job_general.png new file mode 100644 index 000000000..3550a206f Binary files /dev/null and b/docs/en_US/images/dbms_job_general.png differ diff --git a/docs/en_US/images/dbms_job_predefined.png b/docs/en_US/images/dbms_job_predefined.png new file mode 100644 index 000000000..9c7d67241 Binary files /dev/null and b/docs/en_US/images/dbms_job_predefined.png differ diff --git a/docs/en_US/images/dbms_job_repeat.png b/docs/en_US/images/dbms_job_repeat.png new file mode 100644 index 000000000..4abb69845 Binary files /dev/null and b/docs/en_US/images/dbms_job_repeat.png differ diff --git a/docs/en_US/images/dbms_job_scheduler.png b/docs/en_US/images/dbms_job_scheduler.png new file mode 100644 index 000000000..8b185d595 Binary files /dev/null and b/docs/en_US/images/dbms_job_scheduler.png differ diff --git a/docs/en_US/images/dbms_job_sql.png b/docs/en_US/images/dbms_job_sql.png new file mode 100644 index 000000000..ebcbfadef Binary files /dev/null and b/docs/en_US/images/dbms_job_sql.png differ diff --git a/docs/en_US/images/dbms_program_action.png b/docs/en_US/images/dbms_program_action.png new file mode 100644 index 000000000..83a598dcc Binary files /dev/null and b/docs/en_US/images/dbms_program_action.png differ diff --git a/docs/en_US/images/dbms_program_arguments.png b/docs/en_US/images/dbms_program_arguments.png new file mode 100644 index 000000000..c16e5da5b Binary files /dev/null and b/docs/en_US/images/dbms_program_arguments.png differ diff --git a/docs/en_US/images/dbms_program_code.png b/docs/en_US/images/dbms_program_code.png new file mode 100644 index 000000000..dcaba6e45 Binary files /dev/null and b/docs/en_US/images/dbms_program_code.png differ diff --git a/docs/en_US/images/dbms_program_general.png b/docs/en_US/images/dbms_program_general.png new file mode 100644 index 000000000..8d452e551 Binary files /dev/null and b/docs/en_US/images/dbms_program_general.png differ diff --git a/docs/en_US/images/dbms_program_sql.png b/docs/en_US/images/dbms_program_sql.png new file mode 100644 index 000000000..7e8cce743 Binary files /dev/null and b/docs/en_US/images/dbms_program_sql.png differ diff --git a/docs/en_US/images/dbms_schedule_general.png b/docs/en_US/images/dbms_schedule_general.png new file mode 100644 index 000000000..51014d3b3 Binary files /dev/null and b/docs/en_US/images/dbms_schedule_general.png differ diff --git a/docs/en_US/images/dbms_schedule_repeat.png b/docs/en_US/images/dbms_schedule_repeat.png new file mode 100644 index 000000000..6438ab7f2 Binary files /dev/null and b/docs/en_US/images/dbms_schedule_repeat.png differ diff --git a/docs/en_US/images/dbms_schedule_sql.png b/docs/en_US/images/dbms_schedule_sql.png new file mode 100644 index 000000000..b7e81556e Binary files /dev/null and b/docs/en_US/images/dbms_schedule_sql.png differ diff --git a/docs/en_US/managing_database_objects.rst b/docs/en_US/managing_database_objects.rst index 25200a7ab..5b2975685 100644 --- a/docs/en_US/managing_database_objects.rst +++ b/docs/en_US/managing_database_objects.rst @@ -18,6 +18,7 @@ node, and select *Create Cast...* :maxdepth: 1 cast_dialog + dbms_job_scheduler collation_dialog domain_dialog domain_constraint_dialog diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py index 268999bf3..35bb68711 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py @@ -128,6 +128,9 @@ class DatabaseModule(CollectionNodeModule): from .subscriptions import blueprint as module self.submodules.append(module) + from .dbms_job_scheduler import blueprint as module + self.submodules.append(module) + super().register(app, options) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/__init__.py new file mode 100644 index 000000000..f7ece6d6e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/__init__.py @@ -0,0 +1,280 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" Implements DBMS Job Scheduler objects Node.""" + +from functools import wraps +from flask import render_template +from flask_babel import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.server_groups.servers import databases +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import (make_json_response, internal_server_error, + make_response as ajax_response) +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID + + +class DBMSJobSchedulerModule(CollectionNodeModule): + """ + class DBMSJobSchedulerModule(CollectionNodeModule) + + A module class for DBMS Job Scheduler objects node derived + from CollectionNodeModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the DBMS Job Scheduler objects, and it's + base module. + + * get_nodes(gid, sid, did, scid, coid) + - Method is used to generate the browser collection node. + + * script_load() + - Load the module script for DBMS Job Scheduler objects, when any of the + server node is initialized. + + * backend_supported(manager, **kwargs) + + * registert(self, app, options) + - Override the default register function to automagically register + sub-modules at once. + """ + _NODE_TYPE = 'dbms_job_scheduler' + _COLLECTION_LABEL = gettext("DBMS Job Scheduler") + + def __init__(self, *args, **kwargs): + """ + Method is used to initialize the DBMSJobSchedulerModule, and it's base + module. + + Args: + *args: + **kwargs: + """ + super().__init__(*args, **kwargs) + self.min_ver = None + self.max_ver = None + + @property + def node_icon(self): + """ + icon to be displayed for the browser nodes + """ + return 'icon-coll-dbms_job_scheduler' + + def get_nodes(self, gid, sid, did): + """ + Generate the collection node + """ + if self.show_node: + yield self.generate_browser_node(DBMS_JOB_SCHEDULER_ID, did, + self._COLLECTION_LABEL, None) + + @property + def script_load(self): + """ + Load the module script for server, when any of the database node is + initialized. + """ + return databases.DatabaseModule.node_type + + def backend_supported(self, manager, **kwargs): + """ + Function is used to check the pre-requisite for this node. + Args: + manager: + **kwargs: + + Returns: + + """ + if hasattr(self, 'show_node') and not self.show_node: + return False + + # Get the connection for the respective database + conn = manager.connection(did=kwargs['did']) + # Checking whether both 'edb_job_scheduler' and 'dbms_scheduler' + # extensions are created or not. + status, res = conn.execute_scalar(""" + SELECT COUNT(*) FROM pg_extension WHERE extname IN ( + 'edb_job_scheduler', 'dbms_scheduler') """) + if status and int(res) == 2: + # Get the list of databases specified for the edb_job_scheduler + status, res = conn.execute_scalar(""" + SHOW edb_job_scheduler.database_list""") + # If database is available in the specified list than return True. + if status and res and conn.db in res: + return True + + return False + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + def register(self, app, options): + """ + Override the default register function to automagically register + sub-modules at once. + """ + from .dbms_jobs import blueprint as module + self.submodules.append(module) + + from .dbms_programs import blueprint as module + self.submodules.append(module) + + from .dbms_schedules import blueprint as module + self.submodules.append(module) + + super().register(app, options) + + +blueprint = DBMSJobSchedulerModule(__name__) + + +class DBMSJobSchedulerView(PGChildNodeView): + """ + class DBMSJobSchedulerView(PGChildNodeView) + + A view class for cast node derived from PGChildNodeView. This class is + responsible for all the stuff related to view like + create/update/delete cast, showing properties of cast node, + showing sql in sql pane. + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the DBMSJobSchedulerView, and it's + base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attach + manager,conn & template_path properties to self + + * nodes() + - This function will use to create all the child node within that + collection. Here it will create all the scheduler nodes. + + * properties(gid, sid, did, jsid) + - This function will show the properties of the selected job node + + """ + + node_type = blueprint.node_type + BASE_TEMPLATE_PATH = 'dbms_job_scheduler/ppas/#{0}#' + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'did'} + ] + ids = [ + {'type': 'int', 'id': 'jsid'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties'}, + {'get': 'list'} + ], + 'children': [{ + 'get': 'children' + }], + 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}] + }) + + def _init_(self, **kwargs): + self.conn = None + self.template_path = None + self.manager = None + + super().__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will check the + database connection before running view. It will also attach + manager, conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection(did=kwargs['did']) + # Set the template path for the SQL scripts + self.template_path = self.BASE_TEMPLATE_PATH.format( + self.manager.version) + + # Here args[0] will hold self & kwargs will hold gid,sid,did + return f(*args, **kwargs) + return wrap + + @check_precondition + def nodes(self, gid, sid, did): + """ + This function will use to create all the child nodes within the + collection. + """ + return make_json_response( + data=[], + status=200 + ) + + @check_precondition + def list(self, gid, sid, did): + """ + This function will show the run details of all the jobs. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + """ + try: + sql = render_template( + "/".join([self.template_path, 'get_job_run_details.sql'])) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + return ajax_response( + response=res['rows'], + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def properties(self, gid, sid, did, jsid): + """ + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + jsid: Job Scheduler ID + """ + return make_json_response( + data=[], + status=200 + ) + + +DBMSJobSchedulerView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/__init__.py new file mode 100644 index 000000000..a913d9137 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/__init__.py @@ -0,0 +1,785 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" Implements DBMS Job objects Node.""" + +import json +from functools import wraps + +from flask import render_template, request, jsonify +from flask_babel import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.server_groups.servers import databases +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, gone, \ + make_response as ajax_response, internal_server_error, success_return +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID +from pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \ + import format_arguments_from_db +from pgadmin.browser.server_groups.servers.databases.dbms_job_scheduler.utils \ + import (resolve_calendar_string, create_calendar_string, + get_formatted_program_args) + + +class DBMSJobModule(CollectionNodeModule): + """ + class DBMSJobModule(CollectionNodeModule) + + A module class for DBMS Job objects node derived + from CollectionNodeModule. + + Methods: + ------- + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * script_load() + - Load the module script for DBMS Job objects, when any of + the server node is initialized. + """ + _NODE_TYPE = 'dbms_job' + _COLLECTION_LABEL = gettext("DBMS Jobs") + + @property + def collection_icon(self): + """ + icon to be displayed for the browser collection node + """ + return 'icon-coll-pga_job' + + @property + def node_icon(self): + """ + icon to be displayed for the browser nodes + """ + return 'icon-pga_job' + + def get_nodes(self, gid, sid, did, jsid): + """ + Generate the collection node + """ + if self.show_node: + yield self.generate_browser_collection_node(did) + + @property + def node_inode(self): + """ + Override this property to make the node a leaf node. + + Returns: False as this is the leaf node + """ + return False + + @property + def script_load(self): + """ + Load the module script for server, when any of the database node is + initialized. + """ + return databases.DatabaseModule.node_type + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + +blueprint = DBMSJobModule(__name__) + + +class DBMSJobView(PGChildNodeView): + """ + class DBMSJobView(PGChildNodeView) + + A view class for DBMSJob node derived from PGChildNodeView. + This class is responsible for all the stuff related to view like + updating job node, showing properties, showing sql in sql pane. + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the DBMSJobView, and it's base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + + * list() + - This function is used to list all the job nodes within that + collection. + + * nodes() + - This function will use to create all the child node within that + collection. Here it will create all the job node. + + * properties(gid, sid, did, jsid, jsjobid) + - This function will show the properties of the selected job node + + * create(gid, sid, did, jsid, jsjobid) + - This function will create the new job object + + * msql(gid, sid, did, jsid, jsjobid) + - This function is used to return modified SQL for the + selected job node + + * sql(gid, sid, did, jsid, jsjobid) + - Dummy response for sql panel + + * delete(gid, sid, did, jsid, jsjobid) + - Drops job + """ + + node_type = blueprint.node_type + BASE_TEMPLATE_PATH = 'dbms_jobs/ppas/#{0}#' + PROGRAM_TEMPLATE_PATH = 'dbms_programs/ppas/#{0}#' + SCHEDULE_TEMPLATE_PATH = 'dbms_schedules/ppas/#{0}#' + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'did'}, + {'type': 'int', 'id': 'jsid'} + ] + ids = [ + {'type': 'int', 'id': 'jsjobid'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties', 'delete': 'delete', 'put': 'update'}, + {'get': 'list', 'post': 'create', 'delete': 'delete'} + ], + 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}], + 'msql': [{'get': 'msql'}, {'get': 'msql'}], + 'sql': [{'get': 'sql'}], + 'get_procedures': [{}, {'get': 'get_procedures'}], + 'enable_disable': [{'put': 'enable_disable'}], + 'get_programs': [{}, {'get': 'get_programs'}], + 'get_schedules': [{}, {'get': 'get_schedules'}], + 'run_job': [{'put': 'run_job'}], + }) + + def _init_(self, **kwargs): + self.conn = None + self.template_path = None + self.pr_template_path = None + self.sch_template_path = None + self.manager = None + + super().__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will check the + database connection before running view. It will also attach + manager, conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection(did=kwargs['did']) + # Set the template path for the SQL scripts + self.template_path = self.BASE_TEMPLATE_PATH.format( + self.manager.version) + self.pr_template_path = self.PROGRAM_TEMPLATE_PATH.format( + self.manager.version) + self.sch_template_path = self.SCHEDULE_TEMPLATE_PATH.format( + self.manager.version) + + return f(*args, **kwargs) + + return wrap + + @check_precondition + def list(self, gid, sid, did, jsid): + """ + This function is used to list all the job nodes within + that collection. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL])) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition + def nodes(self, gid, sid, did, jsid, jsjobid=None): + """ + This function is used to create all the child nodes within + the collection. Here it will create all the job nodes. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + res = [] + try: + sql = render_template( + "/".join([self.template_path, self._NODES_SQL])) + + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + if jsjobid is not None: + if len(result['rows']) == 0: + return gone( + errormsg=gettext("Could not find the specified job.") + ) + + row = result['rows'][0] + return make_json_response( + data=self.blueprint.generate_browser_node( + row['jsjobid'], + DBMS_JOB_SCHEDULER_ID, + row['jsjobname'], + is_enabled=row['jsjobenabled'], + icon="icon-pga_job" if row['jsjobenabled'] else + "icon-pga_job-disabled", + description=row['jsjobdesc'] + ) + ) + + for row in result['rows']: + res.append( + self.blueprint.generate_browser_node( + row['jsjobid'], + DBMS_JOB_SCHEDULER_ID, + row['jsjobname'], + is_enabled=row['jsjobenabled'], + icon="icon-pga_job" if row['jsjobenabled'] else + "icon-pga_job-disabled", + description=row['jsjobdesc'] + ) + ) + + return make_json_response( + data=res, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def properties(self, gid, sid, did, jsid, jsjobid): + """ + This function will show the properties of the selected job node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + jsjobid: Job ID + """ + try: + status, data = self._fetch_properties(jsjobid) + if not status: + return data + + return ajax_response( + response=data, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + def _fetch_properties(self, jsjobid): + """ + This function is used to fetch the properties. + Args: + jsjobid: + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + jsjobid=jsjobid + ) + status, res = self.conn.execute_dict(sql) + if not status: + return False, internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return False, gone( + errormsg=gettext("Could not find the specified job.") + ) + + data = res['rows'][0] + + # If 'jsjobscname' and 'jsjobprname' in data then set the jsjobtype + if ('jsjobscname' in data and data['jsjobscname'] is not None and + 'jsjobprname' in data and data['jsjobprname'] is not None): + data['jsjobtype'] = 'p' + else: + data['jsjobtype'] = 's' + + # Resolve the repeat interval string + if 'jsscrepeatint' in data: + (freq, by_date, by_month, by_month_day, by_weekday, by_hour, + by_minute) = resolve_calendar_string( + data['jsscrepeatint']) + + data['jsscfreq'] = freq + data['jsscdate'] = by_date + data['jsscmonths'] = by_month + data['jsscmonthdays'] = by_month_day + data['jsscweekdays'] = by_weekday + data['jsschours'] = by_hour + data['jsscminutes'] = by_minute + + # Get Program's arguments + sql = render_template( + "/".join([self.pr_template_path, self._PROPERTIES_SQL]), + jsprid=data['program_id'] + ) + status, res_program = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res_program) + + # Update the data dictionary. + if len(res_program['rows']) > 0: + # Get the formatted program args + get_formatted_program_args(self.pr_template_path, self.conn, + res_program['rows'][0]) + # Get the job argument value + self.get_job_args_value(self.template_path, self.conn, + data['jsjobname'], + res_program['rows'][0]) + data['jsprarguments'] = res_program['rows'][0]['jsprarguments'] \ + if 'jsprarguments' in res_program['rows'][0] else [] + + return True, data + + @check_precondition + def create(self, gid, sid, did, jsid): + """ + This function will create the job node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + data = json.loads(request.data) + try: + # Get the SQL + sql, _, _ = self.get_sql(None, data) + + status, res = self.conn.execute_void('BEGIN') + if not status: + return internal_server_error(errormsg=res) + + status, res = self.conn.execute_scalar(sql) + if not status: + if self.conn.connected(): + self.conn.execute_void('END') + return internal_server_error(errormsg=res) + + self.conn.execute_void('END') + + # Get the newly created job id + sql = render_template( + "/".join([self.template_path, 'get_job_id.sql']), + job_name=data['jsjobname'], conn=self.conn + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone( + errormsg=gettext("Job creation failed.") + ) + row = res['rows'][0] + + return jsonify( + node=self.blueprint.generate_browser_node( + row['jsjobid'], + DBMS_JOB_SCHEDULER_ID, + row['jsjobname'], + is_enabled=row['jsjobenabled'], + icon="icon-pga_job" if row['jsjobenabled'] else + "icon-pga_job-disabled" + ) + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def update(self, gid, sid, did, jsid, jsjobid=None): + """ + This function will update job object + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + data = request.form if request.form else json.loads( + request.data + ) + + try: + sql, jobname, jobenabled = self.get_sql(jsjobid, data) + + status, res = self.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + + return jsonify( + node=self.blueprint.generate_browser_node( + jsjobid, + DBMS_JOB_SCHEDULER_ID, + jobname, + icon="icon-pga_job" if jobenabled else + "icon-pga_job-disabled" + ) + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def delete(self, gid, sid, did, jsid, jsjobid=None): + """Delete the Job.""" + + if jsjobid is None: + data = request.form if request.form else json.loads( + request.data + ) + else: + data = {'ids': [jsjobid]} + + try: + for jsjobid in data['ids']: + status, data = self._fetch_properties(jsjobid) + if not status: + return data + + jsjobname = data['jsjobname'] + + status, res = self.conn.execute_void( + render_template( + "/".join([self.template_path, self._DELETE_SQL]), + job_name=jsjobname, conn=self.conn + ) + ) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response(success=1) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def msql(self, gid, sid, did, jsid, jsjobid=None): + """ + This function is used to return modified SQL for the + selected job node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + jsjobid: Job ID (optional) + """ + data = {} + for k, v in request.args.items(): + try: + # 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 ('jsjobdesc',): + data[k] = v + else: + data[k] = json.loads(v) + except ValueError: + data[k] = v + + sql, _, _ = self.get_sql(jsjobid, data) + + return make_json_response( + data=sql, + status=200 + ) + + def get_sql(self, jsjobid, data): + """ + This function is used to get the SQL. + """ + sql = '' + name = '' + enabled = True + if jsjobid is None: + name = data['jsjobname'] + enabled = data['jsjobenabled'] + + # Create calendar string for repeat interval + repeat_interval = create_calendar_string( + data['jsscfreq'], data['jsscdate'], data['jsscmonths'], + data['jsscmonthdays'], data['jsscweekdays'], data['jsschours'], + data['jsscminutes']) + + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + job_name=data['jsjobname'], + internal_job_type=data['jsjobtype'], + job_type=data['jsprtype'], + job_action=data['jsprproc'] + if data['jsprtype'] == 'STORED_PROCEDURE' else + data['jsprcode'], + enabled=data['jsjobenabled'], + comments=data['jsjobdesc'], + number_of_arguments=data['jsprnoofargs'], + start_date=data['jsscstart'], + repeat_interval=repeat_interval, + end_date=data['jsscend'], + program_name=data['jsjobprname'], + schedule_name=data['jsjobscname'], + arguments=data['jsprarguments'] if 'jsprarguments' in data + else [], + conn=self.conn + ) + elif jsjobid is not None and 'jsprarguments' in data: + status, res = self._fetch_properties(jsjobid) + if not status: + return res + + name = res['jsjobname'] + enabled = res['jsjobenabled'] + sql = render_template( + "/".join([self.template_path, self._UPDATE_SQL]), + job_name=res['jsjobname'], + changed_value=data['jsprarguments']['changed'], + conn=self.conn + ) + + return sql, name, enabled + + @check_precondition + def sql(self, gid, sid, did, jsid, jsjobid): + """ + This function will generate sql for the sql panel + """ + try: + status, data = self._fetch_properties(jsjobid) + if not status: + return '' + + SQL = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + display_comments=True, + job_name=data['jsjobname'], + internal_job_type=data['jsjobtype'], + job_type=data['jsprtype'], + job_action=data['jsprproc'] + if data['jsprtype'] == 'STORED_PROCEDURE' else + data['jsprcode'], + enabled=data['jsjobenabled'], + comments=data['jsjobdesc'], + number_of_arguments=data['jsprnoofargs'], + start_date=data['jsscstart'], + repeat_interval=data['jsscrepeatint'], + end_date=data['jsscend'], + program_name=data['jsjobprname'], + schedule_name=data['jsjobscname'], + arguments=data['jsprarguments'] if 'jsprarguments' in data + else [], + conn=self.conn + ) + + return ajax_response(response=SQL) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def get_procedures(self, gid, sid, did, jsid=None): + """ + This function will return procedure list + :param gid: group id + :param sid: server id + :param did: database id + :return: + """ + res = [] + sql = render_template("/".join([self.pr_template_path, + 'get_procedures.sql']), + datlastsysoid=self._DATABASE_LAST_SYSTEM_OID) + status, rset = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=rset) + + for row in rset['rows']: + # Get formatted Arguments + frmtd_params, _ = format_arguments_from_db( + self.template_path, self.conn, row) + + res.append({'label': row['proc_name'], + 'value': row['proc_name'], + 'no_of_args': row['number_of_arguments'], + 'arguments': frmtd_params['arguments'] + }) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def enable_disable(self, gid, sid, did, jsid, jsjobid=None): + """ + This function is used to enable/disable job. + """ + data = request.form if request.form else json.loads( + request.data + ) + + status, res = self.conn.execute_void( + render_template( + "/".join([self.pr_template_path, 'enable_disable.sql']), + name=data['job_name'], + is_enable=data['is_enable_job'], conn=self.conn + ) + ) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response( + success=1, + info=gettext("Job enabled") if data['is_enable_job'] else + gettext('Job disabled'), + data={ + 'sid': sid, + 'did': did, + 'jsid': jsid, + 'jsjobid': jsjobid + } + ) + + @check_precondition + def get_programs(self, gid, sid, did, jsid=None): + """ + This function will return procedure list + :param gid: group id + :param sid: server id + :param did: database id + :return: + """ + res = [] + sql = render_template("/".join([self.pr_template_path, + self._NODES_SQL]), + datlastsysoid=self._DATABASE_LAST_SYSTEM_OID) + status, rset = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=rset) + + for row in rset['rows']: + res.append({'label': row['jsprname'], + 'value': row['jsprname']}) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def get_schedules(self, gid, sid, did, jsid=None): + """ + This function will return procedure list + :param gid: group id + :param sid: server id + :param did: database id + :return: + """ + res = [] + sql = render_template("/".join([self.sch_template_path, + self._NODES_SQL])) + status, rset = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=rset) + + for row in rset['rows']: + res.append({'label': row['jsscname'], + 'value': row['jsscname'] + }) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def run_job(self, gid, sid, did, jsid, jsjobid=None): + """ + This function is used to run the job now. + """ + data = request.form if request.form else json.loads( + request.data + ) + + try: + status, res = self.conn.execute_void( + render_template( + "/".join([self.template_path, 'run_job.sql']), + job_name=data['job_name'], conn=self.conn + ) + ) + if not status: + return internal_server_error(errormsg=res) + + return success_return( + message=gettext("Started the Job execution.") + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + def get_job_args_value(self, template_path, conn, jobname, data): + """ + This function is used to get the job arguments value. + Args: + template_path: + conn: + jobname: + data: + + Returns: + + """ + if 'jsprarguments' in data and len(data['jsprarguments']) > 0: + for args in data['jsprarguments']: + sql = render_template( + "/".join([template_path, 'get_job_args_value.sql']), + job_name=jobname, + arg_name=args['argname'], conn=self.conn) + status, res = conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + args['argval'] = res + + +DBMSJobView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.js new file mode 100644 index 000000000..8cd44f380 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.js @@ -0,0 +1,234 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import DBMSJobSchema from './dbms_job.ui'; +import { getNodeAjaxOptions } from '../../../../../../../static/js/node_ajax'; +import getApiInstance from '../../../../../../../../static/js/api_instance'; + + +define('pgadmin.node.dbms_job', [ + 'sources/gettext', 'sources/url_for', 'sources/pgadmin', + 'pgadmin.browser', 'pgadmin.browser.collection', +], function(gettext, url_for, pgAdmin, pgBrowser) { + + if (!pgBrowser.Nodes['coll-dbms_job']) { + pgBrowser.Nodes['coll-dbms_job'] = + pgBrowser.Collection.extend({ + node: 'dbms_job', + label: gettext('DBMS Jobs'), + type: 'coll-dbms_job', + columns: ['jsjobname', 'jsjobenabled', 'jsjobruncount', 'jsjobfailurecount', 'jsjobdesc'], + hasSQL: false, + hasDepends: false, + hasStatistics: false, + hasScriptTypes: [], + canDrop: true, + canDropCascade: false, + }); + } + + if (!pgBrowser.Nodes['dbms_job']) { + pgAdmin.Browser.Nodes['dbms_job'] = pgAdmin.Browser.Node.extend({ + parent_type: 'dbms_job_scheduler', + type: 'dbms_job', + label: gettext('DBMS Job'), + node_image: 'icon-pga_job', + epasHelp: true, + epasURL: 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_bip_guide/03_built-in_packages/15_dbms_scheduler/02_create_job/', + dialogHelp: url_for('help.static', {'filename': 'dbms_job.html'}), + canDrop: true, + hasSQL: true, + hasDepends: false, + hasStatistics: false, + Init: function() { + /* Avoid multiple registration of menus */ + if (this.initialized) + return; + + this.initialized = true; + + pgBrowser.add_menus([{ + name: 'create_dbms_job_on_coll', node: 'coll-dbms_job', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Job...'), + data: {action: 'create'}, + },{ + name: 'create_dbms_job', node: 'dbms_job', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Job...'), + data: {action: 'create'}, + },{ + name: 'create_dbms_job', node: 'dbms_job_scheduler', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Job...'), + data: {action: 'create'}, + }, { + name: 'enable_job', node: 'dbms_job', module: this, + applies: ['object', 'context'], callback: 'enable_job', + priority: 4, label: gettext('Enable Job'), + enable : 'is_enabled',data: { + data_disabled: gettext('Job is already enabled.'), + }, + }, { + name: 'disable_job', node: 'dbms_job', module: this, + applies: ['object', 'context'], callback: 'disable_job', + priority: 4, label: gettext('Disable Job'), + enable : 'is_disabled',data: { + data_disabled: gettext('Job is already disabled.'), + }, + }, { + name: 'run_job', node: 'dbms_job', module: this, + applies: ['object', 'context'], callback: 'run_job', + priority: 4, label: gettext('Run Job'), + enable : 'is_disabled', data: { + data_disabled: gettext('Job is already disabled.'), + } + } + ]); + }, + is_enabled: function(node) { + return !node?.is_enabled; + }, + is_disabled: function(node) { + return node?.is_enabled; + }, + callbacks: { + enable_job: function(args, notify) { + let input = args || {}, + obj = this, + t = pgBrowser.tree, + i = 'item' in input ? input.item : t.selected(), + d = i ? t.itemData(i) : undefined; + + if (d) { + notify = notify || _.isUndefined(notify) || _.isNull(notify); + let enable = function() { + let data = d; + getApiInstance().put( + obj.generate_url(i, 'enable_disable', d, true), + {'job_name': data.label, 'is_enable_job': true} + ).then(({data: res})=> { + if (res.success == 1) { + pgAdmin.Browser.notifier.success(res.info); + t.removeIcon(i); + data.icon = 'icon-pga_jobstep'; + data.is_enabled = true; + t.addIcon(i, {icon: data.icon}); + t.updateAndReselectNode(i, data); + } + }).catch(function(error) { + pgAdmin.Browser.notifier.pgRespErrorNotify(error); + t.refresh(i); + }); + }; + if (notify) { + pgAdmin.Browser.notifier.confirm( + gettext('Enable Job'), + gettext('Are you sure you want to enable the job %s?', d.label), + function() { enable(); }, + function() { return true;}, + ); + } else { + enable(); + } + } + + return false; + }, + disable_job: function(args, notify) { + let input = args || {}, + obj = this, + t = pgBrowser.tree, + i = 'item' in input ? input.item : t.selected(), + d = i ? t.itemData(i) : undefined; + + if (d) { + notify = notify || _.isUndefined(notify) || _.isNull(notify); + let disable = function() { + let data = d; + getApiInstance().put( + obj.generate_url(i, 'enable_disable', d, true), + {'job_name': data.label, 'is_enable_job': false} + ).then(({data: res})=> { + if (res.success == 1) { + pgAdmin.Browser.notifier.success(res.info); + t.removeIcon(i); + data.icon = 'icon-pga_jobstep-disabled'; + data.is_enabled = false; + t.addIcon(i, {icon: data.icon}); + t.updateAndReselectNode(i, data); + } + }).catch(function(error) { + pgAdmin.Browser.notifier.pgRespErrorNotify(error); + t.refresh(i); + }); + }; + if (notify) { + pgAdmin.Browser.notifier.confirm( + gettext('Disable Job'), + gettext('Are you sure you want to disable the job %s?', d.label), + function() { disable(); }, + function() { return true;}, + ); + } else { + disable(); + } + } + return false; + }, + run_job: function(args, notify) { + let input = args || {}, + obj = this, + t = pgBrowser.tree, + i = 'item' in input ? input.item : t.selected(), + d = i ? t.itemData(i) : undefined; + + if (d) { + notify = notify || _.isUndefined(notify) || _.isNull(notify); + let run = function() { + let data = d; + getApiInstance().put( + obj.generate_url(i, 'run_job', d, true), + {'job_name': data.label} + ).then(({data: res})=> { + if (res.success == 1) { + pgAdmin.Browser.notifier.success(res.info); + } + }).catch(function(error) { + pgAdmin.Browser.notifier.pgRespErrorNotify(error); + }); + }; + if (notify) { + pgAdmin.Browser.notifier.confirm( + gettext('Run Job'), + gettext('Are you sure you want to run the job %s now?', d.label), + function() { run(); }, + function() { return true;}, + ); + } else { + run(); + } + } + return false; + } + }, + getSchema: function(treeNodeInfo, itemNodeData) { + return new DBMSJobSchema( + { + procedures: ()=>getNodeAjaxOptions('get_procedures', this, treeNodeInfo, itemNodeData), + programs: ()=>getNodeAjaxOptions('get_programs', this, treeNodeInfo, itemNodeData), + schedules: ()=>getNodeAjaxOptions('get_schedules', this, treeNodeInfo, itemNodeData) + } + ); + }, + }); + } + + return pgBrowser.Nodes['dbms_job']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.ui.js new file mode 100644 index 000000000..e6acd3f51 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job.ui.js @@ -0,0 +1,198 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { isEmptyString } from 'sources/validators'; +import moment from 'moment'; +import { getActionSchema, getRepeatSchema } from '../../../static/js/dbms_job_scheduler_common.ui'; + + +export default class DBMSJobSchema extends BaseUISchema { + constructor(fieldOptions={}) { + super({ + jsjobid: null, + jsjobname: '', + jsjobenabled: true, + jsjobdesc: '', + jsjobtype: 's', + jsjobruncount: 0, + jsjobfailurecount: 0, + // Program Args + jsjobprname: '', + jsprtype: 'PLSQL_BLOCK', + jsprenabled: true, + jsprnoofargs: 0, + jsprproc: null, + jsprcode: null, + jsprarguments: [], + // Schedule args + jsjobscname: '', + jsscstart: null, + jsscend: null, + jsscrepeatint: '', + jsscfreq: null, + jsscdate: null, + jsscweekdays: null, + jsscmonthdays: null, + jsscmonths: null, + jsschours: null, + jsscminutes: null, + }); + this.fieldOptions = { + procedures: [], + programs: [], + schedules: [], + ...fieldOptions, + }; + } + + get idAttribute() { + return 'jsjobid'; + } + + get baseFields() { + let obj = this; + return [ + { + id: 'jsjobid', label: gettext('ID'), type: 'int', mode: ['properties'], + readonly: function(state) {return !obj.isNew(state); }, + }, { + id: 'jsjobname', label: gettext('Name'), cell: 'text', + editable: false, type: 'text', noEmpty: true, + readonly: function(state) {return !obj.isNew(state); }, + }, { + id: 'jsjobenabled', label: gettext('Enabled?'), type: 'switch', cell: 'switch', + readonly: function(state) {return !obj.isNew(state); }, + }, { + id: 'jsjobtype', label: gettext('Job Type'), + type: ()=>{ + let options = [ + {'label': gettext('SELF-CONTAINED'), 'value': 's'}, + {'label': gettext('PRE-DEFINED'), 'value': 'p'}, + ]; + return { + type: 'toggle', + options: options, + }; + }, + readonly: function(state) {return !obj.isNew(state); }, + helpMessage: gettext('If the Job Type is Self-Contained you need to specify the action and repeat interval in the Action and Repeat tabs respectively. If the Job Type is Pre-Defined you need to specify the existing Program and Schedule names in the Pre-Defined tab.'), + helpMessageMode: ['create'], + }, { + id: 'jsjobruncount', label: gettext('Run Count'), type: 'int', + readonly: true, mode: ['edit', 'properties'] + }, { + id: 'jsjobfailurecount', label: gettext('Failure Count'), type: 'int', + readonly: true, mode: ['edit', 'properties'] + }, { + id: 'jsjobdesc', label: gettext('Comment'), type: 'multiline', + readonly: function(state) {return !obj.isNew(state); }, + }, + // Add the Action Schema + ...getActionSchema(obj, 'job'), + // Add the Repeat Schema. + ...getRepeatSchema(obj, 'job'), + { + id: 'jsjobprname', label: gettext('Program Name'), type: 'select', + controlProps: { allowClear: false}, group: gettext('Pre-Defined'), + options: this.fieldOptions.programs, + readonly: function(state) { + return !obj.isNew(state) || state.jsjobtype == 's'; + }, + deps: ['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 's') { + return { jsjobprname: null }; + } + } + }, { + id: 'jsjobscname', label: gettext('Schedule Name'), type: 'select', + controlProps: { allowClear: false}, group: gettext('Pre-Defined'), + options: this.fieldOptions.schedules, + readonly: function(state) { + return !obj.isNew(state) || state.jsjobtype == 's'; + }, + deps: ['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 's') { + return { jsjobscname: null }; + } + } + }, + ]; + } + + validate(state, setError) { + if (state.jsjobtype == 's' ) { + if (isEmptyString(state.jsprtype)) { + setError('jsprtype', gettext('Job Type cannot be empty.')); + return true; + } else { + setError('jsprtype', null); + } + + if (state.jsprtype == 'PLSQL_BLOCK' && isEmptyString(state.jsprcode)) { + setError('jsprcode', gettext('Code cannot be empty.')); + return true; + } else { + setError('jsprcode', null); + } + + if (state.jsprtype == 'STORED_PROCEDURE' && isEmptyString(state.jsprproc)) { + setError('jsprproc', gettext('Procedure cannot be empty.')); + return true; + } else { + setError('jsprproc', null); + } + + if (isEmptyString(state.jsscstart) && isEmptyString(state.jsscfreq) && + isEmptyString(state.jsscmonths) && isEmptyString(state.jsscweekdays) && + isEmptyString(state.jsscmonthdays) && isEmptyString(state.jsschours) && + isEmptyString(state.jsscminutes) && isEmptyString(state.jsscdate)) { + setError('jsscstart', gettext('Either Start time or Repeat interval must be specified.')); + return true; + } else { + setError('jsscstart', null); + } + + if (!isEmptyString(state.jsscend)) { + let start_time = state.jsscstart, + end_time = state.jsscend, + start_time_js = start_time.split(' '), + end_time_js = end_time.split(' '); + + start_time_js = moment(start_time_js[0] + ' ' + start_time_js[1]); + end_time_js = moment(end_time_js[0] + ' ' + end_time_js[1]); + + if(end_time_js.isBefore(start_time_js)) { + setError('jsscend', gettext('Start time must be less than end time')); + return true; + } else { + setError('jsscend', null); + } + } else { + state.jsscend = null; + } + } else if (state.jsjobtype == 'p') { + if (isEmptyString(state.jsjobprname)) { + setError('jsjobprname', gettext('Pre-Defined program name cannot be empty.')); + return true; + } else { + setError('jsjobprname', null); + } + if (isEmptyString(state.jsjobscname)) { + setError('jsjobscname', gettext('Pre-Defined schedule name cannot be empty.')); + return true; + } else { + setError('jsjobscname', null); + } + } + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/create.sql new file mode 100644 index 000000000..aac0f82b4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/create.sql @@ -0,0 +1,61 @@ +{% if display_comments %} +-- DBMS Job: '{{ job_name }}' + +-- EXEC dbms_scheduler.DROP_JOB('{{ job_name }}'); + +{% endif %} +{% if internal_job_type is defined and internal_job_type == 's' %} +EXEC dbms_scheduler.CREATE_JOB( + job_name => {{ job_name|qtLiteral(conn) }}, + job_type => {{ job_type|qtLiteral(conn) }}, + job_action => {{ job_action|qtLiteral(conn) }}, + repeat_interval => {{ repeat_interval|qtLiteral(conn) }}{% if start_date or end_date or number_of_arguments or enabled or comments %},{% endif %} +{% if start_date %} + + start_date => {{ start_date|qtLiteral(conn) }}{% if end_date or number_of_arguments or enabled or comments %},{% endif %} +{% endif %} +{% if end_date %} + + end_date => {{ end_date|qtLiteral(conn) }}{% if number_of_arguments or enabled or comments %},{% endif %} +{% endif %} +{% if number_of_arguments %} + + number_of_arguments => {{ number_of_arguments }}{% if enabled or comments %},{% endif %} +{% endif %} +{% if enabled %} + + enabled => {{ enabled }}{% if comments %},{% endif %} +{% endif %} +{% if comments %} + + comments => {{ comments|qtLiteral(conn) }} +{% endif %} +); +{% elif internal_job_type is defined and internal_job_type == 'p' %} +EXEC dbms_scheduler.CREATE_JOB( + job_name => {{ job_name|qtLiteral(conn) }}, + program_name => {{ program_name|qtLiteral(conn) }}, + schedule_name => {{ schedule_name|qtLiteral(conn) }}{% if enabled or comments %},{% endif %} +{% if enabled %} + + enabled => {{ enabled }}{% if comments %},{% endif %} +{% endif %} +{% if comments %} + + comments => {{ comments|qtLiteral(conn) }} +{% endif %} +); +{% endif %} + +{% for args_list_item in arguments %} +EXEC dbms_scheduler.SET_JOB_ARGUMENT_VALUE( + job_name => {{ job_name|qtLiteral(conn) }}, + argument_name => {{ args_list_item.argname|qtLiteral(conn) }}, +{% if args_list_item.argval is defined and args_list_item.argval != '' %} + argument_value => {{ args_list_item.argval|qtLiteral(conn) }} +{% elif args_list_item.argdefval is defined %} + argument_value => {{ args_list_item.argdefval|qtLiteral(conn) }} +{% endif %} +); + +{% endfor %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/delete.sql new file mode 100644 index 000000000..00342ead0 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/delete.sql @@ -0,0 +1,3 @@ +EXEC dbms_scheduler.DROP_JOB( + {{ job_name|qtLiteral(conn) }} +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_args_value.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_args_value.sql new file mode 100644 index 000000000..2b187515e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_args_value.sql @@ -0,0 +1,3 @@ +SELECT value +FROM dba_scheduler_job_args +WHERE job_name = {{ job_name|qtLiteral(conn) }} AND argument_name = {{ arg_name|qtLiteral(conn) }} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_id.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_id.sql new file mode 100644 index 000000000..240cfec8f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/get_job_id.sql @@ -0,0 +1,5 @@ +SELECT + dsj_job_id AS jsjobid, dsj_job_name AS jsjobname, + dsj_enabled AS jsjobenabled +FROM sys.scheduler_0400_job +WHERE dsj_job_name={{ job_name|qtLiteral(conn) }} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/nodes.sql new file mode 100644 index 000000000..7e5a5fcce --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/nodes.sql @@ -0,0 +1,4 @@ +SELECT + dsj_job_id as jsjobid, dsj_job_name as jsjobname, + dsj_enabled as jsjobenabled, dsj_comments as jsjobdesc +FROM sys.scheduler_0400_job; diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/properties.sql new file mode 100644 index 000000000..baa85c808 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/properties.sql @@ -0,0 +1,14 @@ +SELECT + job.dsj_job_id as jsjobid, job_name as jsjobname, program_name as jsjobprname, job_type as jsprtype, + CASE WHEN job_type = 'PLSQL_BLOCK' THEN job_action ELSE '' END AS jsprcode, + CASE WHEN job_type = 'STORED_PROCEDURE' THEN job_action ELSE '' END AS jsprproc, + job_action, number_of_arguments as jsprnoofargs, schedule_name as jsjobscname, + start_date as jsscstart, end_date as jsscend, repeat_interval as jsscrepeatint, + enabled as jsjobenabled, comments as jsjobdesc, + run_count as jsjobruncount, failure_count as jsjobfailurecount, + job.dsj_program_id as program_id, job.dsj_schedule_id as schedule_id +FROM sys.dba_scheduler_jobs jobv + LEFT JOIN sys.scheduler_0400_job job ON jobv.job_name = job.dsj_job_name +{% if jsjobid %} +WHERE job.dsj_job_id={{jsjobid}}::oid +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/run_job.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/run_job.sql new file mode 100644 index 000000000..ad5e857e3 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/run_job.sql @@ -0,0 +1,3 @@ +EXEC dbms_scheduler.RUN_JOB( + {{ job_name|qtLiteral(conn) }} +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/update.sql new file mode 100644 index 000000000..eeb886e50 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/templates/dbms_jobs/ppas/16_plus/update.sql @@ -0,0 +1,12 @@ +{% for chval in changed_value %} +EXEC dbms_scheduler.SET_JOB_ARGUMENT_VALUE( + job_name => {{ job_name|qtLiteral(conn) }}, + argument_name => {{ chval.argname|qtLiteral(conn) }}, +{% if chval.argval is defined and chval.argval != '' %} + argument_value => {{ chval.argval|qtLiteral(conn) }} +{% elif chval.argdefval is defined %} + argument_value => {{ chval.argdefval|qtLiteral(conn) }} +{% endif %} +); + +{% endfor %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/dbms_jobs_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/dbms_jobs_test_data.json new file mode 100644 index 000000000..5216e2bf1 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/dbms_jobs_test_data.json @@ -0,0 +1,429 @@ +{ + "dbms_create_job": [ + { + "name": "Create job when type is self contained and PLSQL_BLOCK", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "test_data": { + "jsjobid": null, + "jsjobname": "job_self_with_psql", + "jsjobenabled": true, + "jsjobdesc": "This is a self contained psql job.", + "jsjobtype": "s", + "jsprtype": "PLSQL_BLOCK", + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2054-02-28 00:00:00 +05:30", + "jsscfreq": "YEARLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"], + "jsjobprname": "", + "jsjobscname": "" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create job when type is self contained and STORED_PROCEDURE without arguments", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "test_data": { + "jsjobid": null, + "jsjobname": "job_self_with_proc_noargs", + "jsjobenabled": true, + "jsjobdesc": "This is a self contained stored procedure job with no args.", + "jsjobtype": "s", + "jsprtype": "STORED_PROCEDURE", + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprproc": "public.test_proc_without_args", + "jsprcode": null, + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2054-02-28 00:00:00 +05:30", + "jsscfreq": "YEARLY", + "jsscdate": "20250113", + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [], + "jsjobprname": "", + "jsjobscname": "" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create job when type is pre-defined and program is PLSQL", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "test_data": { + "jsjobid": null, + "jsjobname": "job_pre_with_psql", + "jsjobenabled": true, + "jsjobdesc": "This is a pre-defined job with PLSQL program.", + "jsjobtype": "p", + "jsjobprname": "prg_with_psql", + "jsjobscname": "yearly_sch", + "jsprtype": null, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprproc": null, + "jsprcode": null, + "jsscstart": null, + "jsscend": null, + "jsscfreq": null, + "jsscdate": null, + "jsscweekdays": null, + "jsscmonthdays": null, + "jsscmonths": null, + "jsschours": null, + "jsscminutes": null + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create job when type is pre-defined and program is Stored Procedure without args", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "test_data": { + "jsjobid": null, + "jsjobname": "job_pre_with_proc_noargs", + "jsjobenabled": true, + "jsjobdesc": "This is a pre-defined job with Stored Procedure without args", + "jsjobtype": "p", + "jsjobprname": "prg_with_proc_noargs", + "jsjobscname": "yearly_sch", + "jsprtype": null, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprproc": null, + "jsprcode": null, + "jsscstart": null, + "jsscend": null, + "jsscfreq": null, + "jsscdate": null, + "jsscweekdays": null, + "jsscmonthdays": null, + "jsscmonths": null, + "jsschours": null, + "jsscminutes": null + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create job when type is pre-defined and program is Stored Procedure with args", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "test_data": { + "jsjobid": null, + "jsjobname": "job_pre_with_proc_args", + "jsjobenabled": true, + "jsjobdesc": "This is a pre-defined job with program is Stored Procedure with args.", + "jsjobtype": "p", + "jsjobprname": "prg_with_proc_args", + "jsjobscname": "yearly_sch", + "jsprtype": null, + "jsprnoofargs": [], + "jsprarguments": [], + "jsprproc": null, + "jsprcode": null, + "jsscstart": null, + "jsscend": null, + "jsscfreq": null, + "jsscdate": null, + "jsscweekdays": null, + "jsscmonthdays": null, + "jsscmonths": null, + "jsschours": null, + "jsscminutes": null + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create job: while server is down", + "url": "/browser/dbms_job/obj/", + "is_positive_test": false, + "test_data": { + "jsjobid": null, + "jsjobname": "job_with_psql", + "jsjobenabled": true, + "jsjobdesc": "This is a self contained psql job.", + "jsjobtype": "s", + "jsprtype": "PLSQL_BLOCK", + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2054-02-28 00:00:00 +05:30", + "jsscfreq": "YEARLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"], + "jsjobprname": "", + "jsjobscname": "" + }, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_scalar", + "return_value": "[(False,'Mocked Internal Server Error')]" + }, + "expected_data": { + "status_code": 500, + "error_msg": "Mocked Internal Server Error", + "test_result_data": {} + } + } + ], + "dbms_update_job": [ + { + "name": "Set job argument value", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "test_data": { + "jsjobname": "job_with_update_args", + "jsprarguments": { + "changed": [{"argid":0,"argtype":"bigint","argmode":"IN","argname":"salary","argdefval":"10000","argval":"5000"}] + } + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + } + ], + "dbms_delete_job": [ + { + "name": "Delete job: With existing DBMS job.", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Delete multiple jobs: With existing DBMS jobs.", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": true + } + ], + "dbms_get_job": [ + { + "name": "Get job: With existing DBMS job.", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Get jobs: With multiple existing DBMS jobs.", + "url": "/browser/dbms_job/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": true + }, + { + "name": "Get job: while server down.", + "url": "/browser/dbms_job/obj/", + "is_positive_test": false, + "inventory_data": {}, + "test_data": {}, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_dict", + "return_value": "(False,'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500, + "error_msg": "Mocked Internal Server Error", + "test_result_data": {} + }, + "is_list": false + } + ], + "dbms_msql_job": [ + { + "name": "Get job msql: For existing PLSQL job.", + "url": "/browser/dbms_job/msql/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": { + "jsjobid": null, + "jsjobname": "job_self_with_psql", + "jsjobenabled": true, + "jsjobdesc": "This is a self contained psql job.", + "jsjobtype": "s", + "jsprtype": "PLSQL_BLOCK", + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2054-02-28 00:00:00 +05:30", + "jsscfreq": "YEARLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"], + "jsjobprname": "", + "jsjobscname": "" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Get job msql: For existing STORED_PROCEDURE job.", + "url": "/browser/dbms_job/msql/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": { + "jsjobid": null, + "jsjobname": "job_self_with_proc_noargs", + "jsjobenabled": true, + "jsjobdesc": "This is a self contained stored procedure job with no args.", + "jsjobtype": "s", + "jsprtype": "STORED_PROCEDURE", + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprproc": "public.test_proc_without_args", + "jsprcode": null, + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2054-02-28 00:00:00 +05:30", + "jsscfreq": "YEARLY", + "jsscdate": "20250113", + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [], + "jsjobprname": "", + "jsjobscname": "" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + } + ], + "dbms_enable_job": [ + { + "name": "Enable existing job", + "url": "/browser/dbms_job/enable_disable/", + "is_positive_test": true, + "test_data": { + "is_enable_job": true + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + } + ], + "dbms_disable_job": [ + { + "name": "Disable existing job", + "url": "/browser/dbms_job/enable_disable/", + "is_positive_test": true, + "test_data": { + "is_enable_job": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_add_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_add_job.py new file mode 100644 index 000000000..4f4bbf2fe --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_add_job.py @@ -0,0 +1,94 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os +import json +from unittest.mock import patch +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSAddJobTestCase(BaseTestGenerator): + """This class will test the add job in the DBMS Job API""" + scenarios = utils.generate_scenarios("dbms_create_job", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + # Create job schedule + job_scheduler_utils.create_dbms_schedule(self, 'yearly_sch') + job_scheduler_utils.create_dbms_program(self, 'prg_with_psql') + job_scheduler_utils.create_dbms_program( + self,'prg_with_proc_noargs', with_proc=True, + proc_name='public.test_proc_without_args()') + job_scheduler_utils.create_dbms_program( + self,'prg_with_proc_args', with_proc=True, + proc_name='public.test_proc_with_args(IN salary bigint DEFAULT ' + '10000, IN name character varying)') + + def runTest(self): + """ This function will add DBMS Job under test database. """ + if self.is_positive_test: + response = job_scheduler_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + + # Verify in backend + response_data = json.loads(response.data) + self.jobs_id = response_data['node']['_id'] + jobs_name = response_data['node']['label'] + is_present = job_scheduler_utils.verify_dbms_job( + self, jobs_name) + self.assertTrue( + is_present,"DBMS job was not created successfully.") + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=eval(self.mock_data["return_value"])): + response = job_scheduler_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_delete_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_delete_job.py new file mode 100644 index 000000000..f8e6a5bd1 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_delete_job.py @@ -0,0 +1,98 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSDeleteJobTestCase(BaseTestGenerator): + """This class will test the delete job in the DBMS job API""" + scenarios = utils.generate_scenarios("dbms_delete_job", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.job_name = "test_job_delete%s" % str(uuid.uuid4())[1:8] + self.job_id = job_scheduler_utils.create_dbms_job( + self, self.job_name) + + # multiple jobs + if self.is_list: + self.job_name2 = "test_job_delete%s" % str(uuid.uuid4())[1:8] + self.job_id_2 = job_scheduler_utils.create_dbms_job( + self, self.job_name2) + + def runTest(self): + """ + This function will test delete DBMS job under test database. + """ + if self.is_list: + self.data['ids'] = [self.job_id, self.job_id_2] + response = job_scheduler_utils.api_delete(self, '') + + # Assert response + utils.assert_status_code(self, response) + + is_present = job_scheduler_utils.verify_dbms_job( + self, self.job_name) + self.assertFalse( + is_present, "DBMS job was not deleted successfully") + + is_present = job_scheduler_utils.verify_dbms_job( + self, self.job_name2) + self.assertFalse( + is_present, "DBMS job was not deleted successfully") + else: + response = job_scheduler_utils.api_delete(self) + + # Assert response + utils.assert_status_code(self, response) + + is_present = job_scheduler_utils.verify_dbms_job( + self, self.job_name) + self.assertFalse( + is_present, "DBMS job was not deleted successfully") + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_disable_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_disable_job.py new file mode 100644 index 000000000..4f9f47487 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_disable_job.py @@ -0,0 +1,71 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSDisableJobTestCase(BaseTestGenerator): + """This class will test the add job in the DBMS job API""" + scenarios = utils.generate_scenarios("dbms_disable_job", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.job_name = "test_job_disable%s" % str(uuid.uuid4())[1:8] + self.data['job_name'] = self.job_name + self.job_id = job_scheduler_utils.create_dbms_job( + self, self.job_name) + + def runTest(self): + """ This function will test DBMS job under test database.""" + response = job_scheduler_utils.api_put(self, self.job_id) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.delete_dbms_job(self, self.job_name) + + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_enable_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_enable_job.py new file mode 100644 index 000000000..78b3ef4b4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_enable_job.py @@ -0,0 +1,71 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSEnableJobTestCase(BaseTestGenerator): + """This class will test the enable job in the DBMS job API""" + scenarios = utils.generate_scenarios("dbms_enable_job", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.job_name = "test_job_enable%s" % str(uuid.uuid4())[1:8] + self.data['job_name'] = self.job_name + self.job_id = job_scheduler_utils.create_dbms_job( + self, self.job_name, False) + + def runTest(self): + """ This function will test DBMS job under test database.""" + response = job_scheduler_utils.api_put(self, self.job_id) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.delete_dbms_job(self, self.job_name) + + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_job.py new file mode 100644 index 000000000..7f28410de --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_job.py @@ -0,0 +1,92 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from unittest.mock import patch +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSGetJobTestCase(BaseTestGenerator): + """This class will test the add job in the DBMS job API""" + scenarios = utils.generate_scenarios("dbms_get_job", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.job_name = "test_job_get%s" % str(uuid.uuid4())[1:8] + self.job_id = job_scheduler_utils.create_dbms_job( + self, self.job_name) + + # multiple jobs + if self.is_list: + self.job_name2 = "test_job_get%s" % str(uuid.uuid4())[1:8] + self.job_id_2 = job_scheduler_utils.create_dbms_job( + self, self.job_name2,) + + def runTest(self): + """ This function will test DBMS job under test database.""" + if self.is_positive_test: + if self.is_list: + response = job_scheduler_utils.api_get(self, '') + else: + response = job_scheduler_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=[eval(self.mock_data["return_value"])]): + response = job_scheduler_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.delete_dbms_job(self, self.job_name) + if self.is_list: + job_scheduler_utils.delete_dbms_job(self, self.job_name2) + + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_msql_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_msql_job.py new file mode 100644 index 000000000..d08ff2c96 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_get_msql_job.py @@ -0,0 +1,65 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSGetMSQLJobTestCase(BaseTestGenerator): + """This class will test the add job in the DBMS job API""" + scenarios = utils.generate_scenarios("dbms_msql_job", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + def runTest(self): + """ This function will add DBMS job under test database. """ + url_encode_data = self.data + + response = job_scheduler_utils.api_get_msql(self, url_encode_data) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_update_job.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_update_job.py new file mode 100644 index 000000000..1a6832b9f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/tests/test_dbms_update_job.py @@ -0,0 +1,73 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_jobs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSUpdateJobTestCase(BaseTestGenerator): + """This class will test the add job in the DBMS Job API""" + scenarios = utils.generate_scenarios("dbms_update_job", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + # Create job schedule + job_scheduler_utils.create_dbms_schedule(self, 'yearly_sch') + job_scheduler_utils.create_dbms_program( + self,'prg_with_proc_args', with_proc=True, + proc_name='public.test_proc_with_args()', + define_args=True) + self.job_id = job_scheduler_utils.create_dbms_job( + self, self.data['jsjobname'], True, + 'prg_with_proc_args','yearly_sch') + + def runTest(self): + """ This function will update DBMS Job under test database. """ + response = job_scheduler_utils.api_put(self, self.job_id) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/__init__.py new file mode 100644 index 000000000..c37220411 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/__init__.py @@ -0,0 +1,574 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" Implements DBMS Program objects Node.""" + +import json +from functools import wraps + +from flask import render_template, request, jsonify +from flask_babel import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.server_groups.servers import databases +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, gone, \ + make_response as ajax_response, internal_server_error +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID +from pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \ + import format_arguments_from_db +from pgadmin.browser.server_groups.servers.databases.dbms_job_scheduler.utils \ + import get_formatted_program_args + + +class DBMSProgramModule(CollectionNodeModule): + """ + class DBMSProgramModule(CollectionNodeModule) + + A module class for DBMS Program objects node derived + from CollectionNodeModule. + + Methods: + ------- + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * script_load() + - Load the module script for DBMS Program objects, when any of + the server node is initialized. + """ + _NODE_TYPE = 'dbms_program' + _COLLECTION_LABEL = gettext("DBMS Programs") + + @property + def collection_icon(self): + """ + icon to be displayed for the browser collection node + """ + return 'icon-coll-pga_jobstep' + + @property + def node_icon(self): + """ + icon to be displayed for the browser nodes + """ + return 'icon-pga_jobstep' + + def get_nodes(self, gid, sid, did, jsid): + """ + Generate the collection node + """ + if self.show_node: + yield self.generate_browser_collection_node(did) + + @property + def node_inode(self): + """ + Override this property to make the node a leaf node. + + Returns: False as this is the leaf node + """ + return False + + @property + def script_load(self): + """ + Load the module script for server, when any of the database node is + initialized. + """ + return databases.DatabaseModule.node_type + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + +blueprint = DBMSProgramModule(__name__) + + +class DBMSProgramView(PGChildNodeView): + """ + class DBMSProgramView(PGChildNodeView) + + A view class for DBMSProgram node derived from PGChildNodeView. + This class is responsible for all the stuff related to view like + updating program node, showing properties, showing sql in sql pane. + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the DBMSProgramView, and it's base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + + * list() + - This function is used to list all the program nodes within that + collection. + + * nodes() + - This function will use to create all the child node within that + collection. Here it will create all the program node. + + * properties(gid, sid, did, jsid, jsprid) + - This function will show the properties of the selected program node + + * create(gid, sid, did, jsid, jsprid) + - This function will create the new program object + + * msql(gid, sid, did, jsid, jsprid) + - This function is used to return modified SQL for the + selected program node + + * sql(gid, sid, did, jsid, jsprid) + - Dummy response for sql panel + + * delete(gid, sid, did, jsid, jsprid) + - Drops job program + """ + + node_type = blueprint.node_type + BASE_TEMPLATE_PATH = 'dbms_programs/ppas/#{0}#' + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'did'}, + {'type': 'int', 'id': 'jsid'} + ] + ids = [ + {'type': 'int', 'id': 'jsprid'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties', 'delete': 'delete'}, + {'get': 'list', 'post': 'create', 'delete': 'delete'} + ], + 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}], + 'msql': [{'get': 'msql'}, {'get': 'msql'}], + 'sql': [{'get': 'sql'}], + 'get_procedures': [{}, {'get': 'get_procedures'}], + 'enable_disable': [{'put': 'enable_disable'}], + }) + + def _init_(self, **kwargs): + self.conn = None + self.template_path = None + self.manager = None + + super().__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will check the + database connection before running view. It will also attach + manager, conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection(did=kwargs['did']) + # Set the template path for the SQL scripts + self.template_path = self.BASE_TEMPLATE_PATH.format( + self.manager.version) + + return f(*args, **kwargs) + + return wrap + + @check_precondition + def list(self, gid, sid, did, jsid): + """ + This function is used to list all the program nodes within + that collection. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL])) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition + def nodes(self, gid, sid, did, jsid, jsprid=None): + """ + This function is used to create all the child nodes within + the collection. Here it will create all the program nodes. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + res = [] + try: + sql = render_template( + "/".join([self.template_path, self._NODES_SQL])) + + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + if jsprid is not None: + if len(result['rows']) == 0: + return gone( + errormsg=gettext("Could not find the specified " + "program.")) + + row = result['rows'][0] + return make_json_response( + data=self.blueprint.generate_browser_node( + row['jsprid'], + DBMS_JOB_SCHEDULER_ID, + row['jsprname'], + is_enabled=row['jsprenabled'], + icon="icon-pga_jobstep" if row['jsprenabled'] else + "icon-pga_jobstep-disabled", + description=row['jsprdesc'] + ) + ) + + for row in result['rows']: + res.append( + self.blueprint.generate_browser_node( + row['jsprid'], + DBMS_JOB_SCHEDULER_ID, + row['jsprname'], + is_enabled=row['jsprenabled'], + icon="icon-pga_jobstep" if row['jsprenabled'] else + "icon-pga_jobstep-disabled", + description=row['jsprdesc'] + ) + ) + + return make_json_response( + data=res, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def properties(self, gid, sid, did, jsid, jsprid): + """ + This function will show the properties of the selected program node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + jsprid: Job program ID + """ + try: + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + jsprid=jsprid + ) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone( + errormsg=gettext("Could not find the specified program.") + ) + + data = res['rows'][0] + # Get the formatted program args + get_formatted_program_args(self.template_path, self.conn, data) + + return ajax_response( + response=data, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def create(self, gid, sid, did, jsid): + """ + This function will update the data for the selected program node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + data = json.loads(request.data) + try: + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + program_name=data['jsprname'], + program_type=data['jsprtype'], + program_action=data['jsprproc'] + if data['jsprtype'] == 'STORED_PROCEDURE' else + data['jsprcode'], + number_of_arguments=data['jsprnoofargs'], + enabled=data['jsprenabled'], + comments=data['jsprdesc'], + arguments=data['jsprarguments'], + conn=self.conn + ) + + status, res = self.conn.execute_void('BEGIN') + if not status: + return internal_server_error(errormsg=res) + + status, res = self.conn.execute_scalar(sql) + if not status: + if self.conn.connected(): + self.conn.execute_void('END') + return internal_server_error(errormsg=res) + + self.conn.execute_void('END') + + # Get the newly created program id + sql = render_template( + "/".join([self.template_path, 'get_program_id.sql']), + jsprname=data['jsprname'], conn=self.conn + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone( + errormsg=gettext("Job program creation failed.") + ) + row = res['rows'][0] + + return jsonify( + node=self.blueprint.generate_browser_node( + row['jsprid'], + DBMS_JOB_SCHEDULER_ID, + row['jsprname'], + is_enabled=row['jsprenabled'], + icon="icon-pga_jobstep" if row['jsprenabled'] else + "icon-pga_jobstep-disabled" + ) + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def delete(self, gid, sid, did, jsid, jsprid=None): + """Delete the Job program.""" + + if jsprid is None: + data = request.form if request.form else json.loads( + request.data + ) + else: + data = {'ids': [jsprid]} + + try: + for jsprid in data['ids']: + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + jsprid=jsprid + ) + + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + jsprname = res['rows'][0]['jsprname'] + + status, res = self.conn.execute_void( + render_template( + "/".join([self.template_path, self._DELETE_SQL]), + program_name=jsprname, conn=self.conn + ) + ) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response(success=1) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def msql(self, gid, sid, did, jsid, jsprid=None): + """ + This function is used to return modified SQL for the + selected program node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + jsprid: Job program ID (optional) + """ + data = {} + for k, v in request.args.items(): + try: + # 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 ('jsprdesc',): + data[k] = v + else: + data[k] = json.loads(v) + except ValueError: + data[k] = v + + try: + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + program_name=data['jsprname'], + program_type=data['jsprtype'], + program_action=data['jsprproc'] + if data['jsprtype'] == 'STORED_PROCEDURE' else + data['jsprcode'], + number_of_arguments=data['jsprnoofargs'], + enabled=data['jsprenabled'], + comments=data['jsprdesc'], + arguments=data['jsprarguments'], + conn=self.conn + ) + + return make_json_response( + data=sql, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def sql(self, gid, sid, did, jsid, jsprid): + """ + This function will generate sql for the sql panel + """ + try: + SQL = render_template("/".join( + [self.template_path, self._PROPERTIES_SQL] + ), jsprid=jsprid) + + status, res = self.conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone( + gettext("Could not find the DBMS Schedule.") + ) + + data = res['rows'][0] + # Get the formatted program args + get_formatted_program_args(self.template_path, self.conn, data) + + SQL = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + display_comments=True, + program_name=data['jsprname'], + program_type=data['jsprtype'], + program_action=data['jsprproc'] + if data['jsprtype'] == 'STORED_PROCEDURE' else + data['jsprcode'], + number_of_arguments=data['jsprnoofargs'], + enabled=data['jsprenabled'], + comments=data['jsprdesc'], + arguments=data['jsprarguments'] if 'jsprarguments' in data + else [], + conn=self.conn + ) + + return ajax_response(response=SQL) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def get_procedures(self, gid, sid, did, jsid=None): + """ + This function will return procedure list + :param gid: group id + :param sid: server id + :param did: database id + :return: + """ + res = [] + sql = render_template("/".join([self.template_path, + 'get_procedures.sql']), + datlastsysoid=self._DATABASE_LAST_SYSTEM_OID) + status, rset = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=rset) + + for row in rset['rows']: + # Get formatted Arguments + frmtd_params, _ = format_arguments_from_db( + self.template_path, self.conn, row) + + res.append({'label': row['proc_name'], + 'value': row['proc_name'], + 'no_of_args': row['number_of_arguments'], + 'arguments': frmtd_params['arguments'] + }) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition + def enable_disable(self, gid, sid, did, jsid, jsprid=None): + """ + This function is used to enable/disable program. + """ + data = request.form if request.form else json.loads( + request.data + ) + + status, res = self.conn.execute_void( + render_template( + "/".join([self.template_path, 'enable_disable.sql']), + name=data['program_name'], + is_enable=data['is_enable_program'], conn=self.conn + ) + ) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response( + success=1, + info=gettext("Program enabled") if data['is_enable_program'] else + gettext('Program disabled'), + data={ + 'sid': sid, + 'did': did, + 'jsid': jsid, + 'jsprid': jsprid + } + ) + + +DBMSProgramView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.js new file mode 100644 index 000000000..160e9165f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.js @@ -0,0 +1,189 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import DBMSProgramSchema from './dbms_program.ui'; +import { getNodeAjaxOptions } from '../../../../../../../static/js/node_ajax'; +import getApiInstance from '../../../../../../../../static/js/api_instance'; + +define('pgadmin.node.dbms_program', [ + 'sources/gettext', 'sources/url_for', 'sources/pgadmin', + 'pgadmin.browser', 'pgadmin.browser.collection', +], function(gettext, url_for, pgAdmin, pgBrowser) { + + if (!pgBrowser.Nodes['coll-dbms_program']) { + pgBrowser.Nodes['coll-dbms_program'] = + pgBrowser.Collection.extend({ + node: 'dbms_program', + label: gettext('DBMS Programs'), + type: 'coll-dbms_program', + columns: ['jsprname', 'jsprtype', 'jsprenabled', 'jsprdesc'], + hasSQL: false, + hasDepends: false, + hasStatistics: false, + hasScriptTypes: [], + canDrop: true, + canDropCascade: false, + }); + } + + if (!pgBrowser.Nodes['dbms_program']) { + pgAdmin.Browser.Nodes['dbms_program'] = pgAdmin.Browser.Node.extend({ + parent_type: 'dbms_job_scheduler', + type: 'dbms_program', + label: gettext('DBMS Program'), + node_image: 'icon-pga_jobstep', + epasHelp: true, + epasURL: 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_bip_guide/03_built-in_packages/15_dbms_scheduler/03_create_program/', + dialogHelp: url_for('help.static', {'filename': 'dbms_program.html'}), + canDrop: true, + hasSQL: true, + hasDepends: false, + hasStatistics: false, + Init: function() { + /* Avoid multiple registration of menus */ + if (this.initialized) + return; + + this.initialized = true; + + pgBrowser.add_menus([{ + name: 'create_dbms_program_on_coll', node: 'coll-dbms_program', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Program...'), + data: {action: 'create'}, + }, { + name: 'create_dbms_program', node: 'dbms_program', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Program...'), + data: {action: 'create'}, + }, { + name: 'create_dbms_program', node: 'dbms_job_scheduler', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Program...'), + data: {action: 'create'}, + }, { + name: 'enable_program', node: 'dbms_program', module: this, + applies: ['object', 'context'], callback: 'enable_program', + priority: 4, label: gettext('Enable Program'), + enable : 'is_enabled',data: { + data_disabled: gettext('Program is already enabled.'), + }, + }, { + name: 'disable_program', node: 'dbms_program', module: this, + applies: ['object', 'context'], callback: 'disable_program', + priority: 4, label: gettext('Disable Program'), + enable : 'is_disabled',data: { + data_disabled: gettext('Program is already disabled.'), + }, + } + ]); + }, + is_enabled: function(node) { + return !node?.is_enabled; + }, + is_disabled: function(node) { + return node?.is_enabled; + }, + callbacks: { + enable_program: function(args, notify) { + let input = args || {}, + obj = this, + t = pgBrowser.tree, + i = 'item' in input ? input.item : t.selected(), + d = i ? t.itemData(i) : undefined; + + if (d) { + notify = notify || _.isUndefined(notify) || _.isNull(notify); + let enable = function() { + let data = d; + getApiInstance().put( + obj.generate_url(i, 'enable_disable', d, true), + {'program_name': data.label, 'is_enable_program': true} + ).then(({data: res})=> { + if (res.success == 1) { + pgAdmin.Browser.notifier.success(res.info); + t.removeIcon(i); + data.icon = 'icon-pga_jobstep'; + data.is_enabled = true; + t.addIcon(i, {icon: data.icon}); + t.updateAndReselectNode(i, data); + } + }).catch(function(error) { + pgAdmin.Browser.notifier.pgRespErrorNotify(error); + t.refresh(i); + }); + }; + if (notify) { + pgAdmin.Browser.notifier.confirm( + gettext('Enable Program'), + gettext('Are you sure you want to enable the program %s?', d.label), + function() { enable(); }, + function() { return true;}, + ); + } else { + enable(); + } + } + + return false; + }, + disable_program: function(args, notify) { + let input = args || {}, + obj = this, + t = pgBrowser.tree, + i = 'item' in input ? input.item : t.selected(), + d = i ? t.itemData(i) : undefined; + + if (d) { + notify = notify || _.isUndefined(notify) || _.isNull(notify); + let disable = function() { + let data = d; + getApiInstance().put( + obj.generate_url(i, 'enable_disable', d, true), + {'program_name': data.label, 'is_enable_program': false} + ).then(({data: res})=> { + if (res.success == 1) { + pgAdmin.Browser.notifier.success(res.info); + t.removeIcon(i); + data.icon = 'icon-pga_jobstep-disabled'; + data.is_enabled = false; + t.addIcon(i, {icon: data.icon}); + t.updateAndReselectNode(i, data); + } + }).catch(function(error) { + pgAdmin.Browser.notifier.pgRespErrorNotify(error); + t.refresh(i); + }); + }; + if (notify) { + pgAdmin.Browser.notifier.confirm( + gettext('Disable Program'), + gettext('Are you sure you want to disable the program %s?', d.label), + function() { disable(); }, + function() { return true;}, + ); + } else { + disable(); + } + } + return false; + }, + }, + getSchema: function(treeNodeInfo, itemNodeData) { + return new DBMSProgramSchema( + { + procedures: ()=>getNodeAjaxOptions('get_procedures', this, treeNodeInfo, itemNodeData), + } + ); + }, + }); + } + + return pgBrowser.Nodes['dbms_program']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.ui.js new file mode 100644 index 000000000..b32ec6415 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program.ui.js @@ -0,0 +1,76 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { isEmptyString } from 'sources/validators'; +import { getActionSchema } from '../../../static/js/dbms_job_scheduler_common.ui'; + +export default class DBMSProgramSchema extends BaseUISchema { + constructor(fieldOptions={}) { + super({ + jsprid: null, + jsprname: '', + jsprtype: 'PLSQL_BLOCK', + jsprenabled: true, + jsprnoofargs: 0, + jsprarguments: [], + jsprdesc: '', + jsprproc: null, + jsprcode: null, + }); + this.fieldOptions = { + procedures: [], + ...fieldOptions, + }; + } + + get idAttribute() { + return 'jsprid'; + } + + get baseFields() { + let obj = this; + return [ + { + id: 'jsprid', label: gettext('ID'), type: 'int', mode: ['properties'], + readonly: function(state) {return !obj.isNew(state); }, + }, { + id: 'jsprname', label: gettext('Name'), cell: 'text', + type: 'text', noEmpty: true, + readonly: function(state) {return !obj.isNew(state); }, + }, { + id: 'jsprenabled', label: gettext('Enabled?'), type: 'switch', cell: 'switch', + readonly: function(state) {return !obj.isNew(state); }, + }, + // Add the Action Schema + ...getActionSchema(obj, 'program'), + { + id: 'jsprdesc', label: gettext('Comment'), type: 'multiline', + readonly: function(state) {return !obj.isNew(state); }, + } + ]; + } + validate(state, setError) { + /* code validation*/ + if (state.jsprtype == 'PLSQL_BLOCK' && isEmptyString(state.jsprcode)) { + setError('jsprcode', gettext('Code cannot be empty.')); + return true; + } else { + setError('jsprcode', null); + } + + if (state.jsprtype == 'STORED_PROCEDURE' && isEmptyString(state.jsprproc)) { + setError('jsprproc', gettext('Procedure cannot be empty.')); + return true; + } else { + setError('jsprproc', null); + } + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/create.sql new file mode 100644 index 000000000..b77843538 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/create.sql @@ -0,0 +1,34 @@ +{% if display_comments %} +-- DBMS Program: '{{ program_name }}' + +-- EXEC dbms_scheduler.DROP_PROGRAM('{{ program_name }}'); + +{% endif %} +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => {{ program_name|qtLiteral(conn) }}, + program_type => {{ program_type|qtLiteral(conn) }}, + program_action => {{ program_action|qtLiteral(conn) }}{% if number_of_arguments or enabled or comments %},{% endif %} +{% if number_of_arguments %} + + number_of_arguments => {{ number_of_arguments }}{% if enabled or comments %},{% endif %} +{% endif %} +{% if enabled %} + + enabled => {{ enabled }}{% if comments %},{% endif %} +{% endif %} +{% if comments %} + + comments => {{ comments|qtLiteral(conn) }} +{% endif %} +); + +{% for args_list_item in arguments %} +EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( + program_name => {{ program_name|qtLiteral(conn) }}, + argument_position => {{ args_list_item['argid'] }}, + argument_name => {{ args_list_item['argname']|qtLiteral(conn) }}, + argument_type => {{ args_list_item['argtype']|qtLiteral(conn) }}, + default_value => {{ args_list_item['argdefval']|qtLiteral(conn) }} +); + +{% endfor %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/delete.sql new file mode 100644 index 000000000..83d61ca47 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/delete.sql @@ -0,0 +1,3 @@ +EXEC dbms_scheduler.DROP_PROGRAM( + {{ program_name|qtLiteral(conn) }} +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/enable_disable.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/enable_disable.sql new file mode 100644 index 000000000..ee43653ee --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/enable_disable.sql @@ -0,0 +1,5 @@ +{% if is_enable %} +EXEC dbms_scheduler.ENABLE({{ name|qtLiteral(conn) }}); +{% else %} +EXEC dbms_scheduler.DISABLE({{ name|qtLiteral(conn) }}); +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_procedures.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_procedures.sql new file mode 100644 index 000000000..07ff4e18d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_procedures.sql @@ -0,0 +1,20 @@ +SELECT + pg_catalog.concat(pg_catalog.quote_ident(nsp.nspname),'.',pg_catalog.quote_ident(pr.proname)) AS proc_name, + pr.pronargs AS number_of_arguments, proargnames, + pg_catalog.oidvectortypes(proargtypes) AS proargtypenames, + pg_catalog.pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) AS proargdefaultvals +FROM + pg_catalog.pg_proc pr +JOIN + pg_catalog.pg_type typ ON typ.oid=prorettype +JOIN + pg_catalog.pg_namespace nsp ON nsp.oid=pr.pronamespace +WHERE + pr.prokind IN ('f', 'p') + AND typname NOT IN ('trigger', 'event_trigger') + AND (pronamespace = 2200::oid OR pronamespace > {{datlastsysoid}}::OID) +{% if without_args %} + AND pr.pronargs = 0 +{% endif %} +ORDER BY + proname; diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_program_id.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_program_id.sql new file mode 100644 index 000000000..9e316fcbc --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/get_program_id.sql @@ -0,0 +1,5 @@ +SELECT + dsp_program_id AS jsprid, dsp_program_name AS jsprname, + dsp_enabled AS jsprenabled +FROM sys.scheduler_0200_program +WHERE dsp_program_name={{ jsprname|qtLiteral(conn) }} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/nodes.sql new file mode 100644 index 000000000..a22c9253c --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/nodes.sql @@ -0,0 +1,6 @@ +SELECT + dsp_program_id AS jsprid, dsp_program_name AS jsprname, + dsp_program_type AS jsprtype, dsp_enabled AS jsprenabled, + dsp_comments AS jsprdesc +FROM sys.scheduler_0200_program prt + JOIN sys.dba_scheduler_programs prv ON prt.dsp_program_name = prv.program_name diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/properties.sql new file mode 100644 index 000000000..58bcdf6bb --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/templates/dbms_programs/ppas/16_plus/properties.sql @@ -0,0 +1,18 @@ +SELECT + dsp_program_id AS jsprid, dsp_program_name AS jsprname, + dsp_program_type AS jsprtype, + CASE WHEN dsp_program_type = 'PLSQL_BLOCK' THEN dsp_program_action ELSE '' END AS jsprcode, + CASE WHEN dsp_program_type = 'STORED_PROCEDURE' THEN dsp_program_action ELSE '' END AS jsprproc, + dsp_number_of_arguments AS jsprnoofargs, dsp_enabled AS jsprenabled, + dsp_comments AS jsprdesc, array_agg(argument_name) AS proargnames, + array_agg(argument_type) AS proargtypenames, + array_agg(default_value) AS proargdefaultvals +FROM sys.scheduler_0200_program prt +{% if not jsprid %} + JOIN sys.dba_scheduler_programs prv ON prt.dsp_program_name = prv.program_name +{% endif %} + LEFT JOIN dba_scheduler_program_args prargs ON dsp_program_name = prargs.program_name +{% if jsprid %} +WHERE dsp_program_id={{jsprid}}::oid +{% endif %} +GROUP BY dsp_program_id, dsp_program_name diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/dbms_programs_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/dbms_programs_test_data.json new file mode 100644 index 000000000..32f884627 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/dbms_programs_test_data.json @@ -0,0 +1,269 @@ +{ + "dbms_create_program": [ + { + "name": "Create program when type is PLSQL_BLOCK", + "url": "/browser/dbms_program/obj/", + "is_positive_test": true, + "test_data": { + "jsprid": null, + "jsprname": "prg_with_psql", + "jsprtype": "PLSQL_BLOCK", + "jsprenabled": true, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprdesc": "This is a PLSQL program.", + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create program when type is STORED_PROCEDURE with args", + "url": "/browser/dbms_program/obj/", + "is_positive_test": true, + "test_data": { + "jsprid": null, + "jsprname": "prg_with_proc_args", + "jsprtype": "STORED_PROCEDURE", + "jsprenabled": true, + "jsprnoofargs": 2, + "jsprarguments": [{"argid":0,"argtype":"bigint","argmode":"IN","argname":"salary","argdefval":"10000"},{"argid":1,"argtype":"character varying","argmode":"IN","argname":"name","argdefval":" -"}], + "jsprdesc": "This is a STORED_PROCEDURE program.", + "jsprproc": "public.test_proc_with_args", + "jsprcode": null + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create program when type is STORED_PROCEDURE without args", + "url": "/browser/dbms_program/obj/", + "proc_name": "public.test_proc_without_args()", + "is_positive_test": true, + "test_data": { + "jsprid": null, + "jsprname": "prg_with_proc_without_args", + "jsprtype": "STORED_PROCEDURE", + "jsprenabled": false, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprdesc": "This is a STORED_PROCEDURE program.", + "jsprproc": "public.test_proc_without_args", + "jsprcode": null + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create program: while server is down", + "url": "/browser/dbms_program/obj/", + "is_positive_test": false, + "test_data": { + "jsprid": null, + "jsprname": "prg_with_psql", + "jsprtype": "PLSQL_BLOCK", + "jsprenabled": true, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprdesc": "This is a PLSQL program.", + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;" + }, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_scalar", + "return_value": "[(False,'Mocked Internal Server Error')]" + }, + "expected_data": { + "status_code": 500, + "error_msg": "Mocked Internal Server Error", + "test_result_data": {} + } + } + ], + "dbms_delete_program": [ + { + "name": "Delete program: With existing DBMS program.", + "url": "/browser/dbms_program/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Delete multiple programs: With existing DBMS programs.", + "url": "/browser/dbms_program/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": true + } + ], + "dbms_get_program": [ + { + "name": "Get program: With existing DBMS program.", + "url": "/browser/dbms_program/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Get programs: With multiple existing DBMS programs.", + "url": "/browser/dbms_program/obj/", + "proc_name": "public.test_proc_with_args(IN salary bigint DEFAULT 10000, IN name character varying)", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": true + }, + { + "name": "Get program: while server down.", + "url": "/browser/dbms_program/obj/", + "is_positive_test": false, + "inventory_data": {}, + "test_data": {}, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_dict", + "return_value": "(False,'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500, + "error_msg": "Mocked Internal Server Error", + "test_result_data": {} + }, + "is_list": false + } + ], + "dbms_msql_program": [ + { + "name": "Get program msql: For existing PLSQL program.", + "url": "/browser/dbms_program/msql/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": { + "jsprid": null, + "jsprname": "prg_with_psql", + "jsprtype": "PLSQL_BLOCK", + "jsprenabled": true, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprdesc": "This is a PLSQL program.", + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Get program msql: For existing STORED_PROCEDURE program.", + "url": "/browser/dbms_program/msql/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": { + "jsprid": null, + "jsprname": "prg_with_proc_args", + "jsprtype": "STORED_PROCEDURE", + "jsprenabled": true, + "jsprnoofargs": 2, + "jsprarguments": "[{\"argid\":0,\"argtype\":\"bigint\",\"argmode\":\"IN\",\"argname\":\"salary\",\"argdefval\":\"10000\"},{\"argid\":1,\"argtype\":\"character varying\",\"argmode\":\"IN\",\"argname\":\"name\",\"argdefval\":\" -\"}]", + "jsprdesc": "This is a STORED_PROCEDURE program.", + "jsprproc": "public.test_proc_with_args", + "jsprcode": null + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + } + ], + "dbms_enable_program": [ + { + "name": "Enable existing program", + "url": "/browser/dbms_program/enable_disable/", + "is_positive_test": true, + "test_data": { + "is_enable_program": true + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + } + ], + "dbms_disable_program": [ + { + "name": "Disable existing program", + "url": "/browser/dbms_program/enable_disable/", + "is_positive_test": true, + "test_data": { + "is_enable_program": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled.sql new file mode 100644 index 000000000..01448feae --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled.sql @@ -0,0 +1,8 @@ +-- DBMS Program: 'dbms_prg_disabled' + +-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_disabled'); + +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_disabled', + program_type => 'PLSQL_BLOCK', + program_action => 'BEGIN PERFORM 1; END;'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled_msql.sql new file mode 100644 index 000000000..5dd28145b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_disabled_msql.sql @@ -0,0 +1,4 @@ +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_disabled', + program_type => 'PLSQL_BLOCK', + program_action => 'BEGIN PERFORM 1; END;'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args.sql new file mode 100644 index 000000000..6f4e6e83f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args.sql @@ -0,0 +1,28 @@ +-- DBMS Program: 'dbms_prg_proc_with_args' + +-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_proc_with_args'); + +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_proc_with_args', + program_type => 'STORED_PROCEDURE', + program_action => 'public.test_proc_with_args', + number_of_arguments => 2, + enabled => True, + comments => 'This is a STORED_PROCEDURE program.' +); + +EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( + program_name => 'dbms_prg_proc_with_args', + argument_position => 0, + argument_name => 'salary', + argument_type => 'bigint', + default_value => '10000' +); + +EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( + program_name => 'dbms_prg_proc_with_args', + argument_position => 1, + argument_name => 'name', + argument_type => 'character varying', + default_value => ' -' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args_msql.sql new file mode 100644 index 000000000..03cf1941e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_with_args_msql.sql @@ -0,0 +1,24 @@ +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_proc_with_args', + program_type => 'STORED_PROCEDURE', + program_action => 'public.test_proc_with_args', + number_of_arguments => 2, + enabled => True, + comments => 'This is a STORED_PROCEDURE program.' +); + +EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( + program_name => 'dbms_prg_proc_with_args', + argument_position => 0, + argument_name => 'salary', + argument_type => 'bigint', + default_value => '10000' +); + +EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( + program_name => 'dbms_prg_proc_with_args', + argument_position => 1, + argument_name => 'name', + argument_type => 'character varying', + default_value => ' -' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args.sql new file mode 100644 index 000000000..d32132e75 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args.sql @@ -0,0 +1,10 @@ +-- DBMS Program: 'dbms_prg_proc_without_args' + +-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_proc_without_args'); + +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_proc_without_args', + program_type => 'STORED_PROCEDURE', + program_action => 'public.test_proc_without_args', + comments => 'This is a STORED_PROCEDURE program.' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args_msql.sql new file mode 100644 index 000000000..43e8a9aa4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_proc_without_args_msql.sql @@ -0,0 +1,6 @@ +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_proc_without_args', + program_type => 'STORED_PROCEDURE', + program_action => 'public.test_proc_without_args', + comments => 'This is a STORED_PROCEDURE program.' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql.sql new file mode 100644 index 000000000..8344fb59e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql.sql @@ -0,0 +1,11 @@ +-- DBMS Program: 'dbms_prg_with_psql' + +-- EXEC dbms_scheduler.DROP_PROGRAM('dbms_prg_with_psql'); + +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_with_psql', + program_type => 'PLSQL_BLOCK', + program_action => 'BEGIN PERFORM 1; END;', + enabled => True, + comments => 'This is a PLSQL program.' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql_msql.sql new file mode 100644 index 000000000..7bffeba44 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/create_program_psql_msql.sql @@ -0,0 +1,7 @@ +EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => 'dbms_prg_with_psql', + program_type => 'PLSQL_BLOCK', + program_action => 'BEGIN PERFORM 1; END;', + enabled => True, + comments => 'This is a PLSQL program.' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/test.json new file mode 100644 index 000000000..67a872d41 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/ppas/16_plus/test.json @@ -0,0 +1,145 @@ +{ + "scenarios": [ + { + "type": "create", + "name": "Create extension 'edb_job_scheduler' for DBMS Program", + "endpoint": "NODE-extension.obj", + "sql_endpoint": "NODE-extension.sql_id", + "data": { + "name": "edb_job_scheduler" + }, + "store_object_id": true + }, + { + "type": "create", + "name": "Create extension 'dbms_scheduler' for DBMS Program", + "endpoint": "NODE-extension.obj", + "sql_endpoint": "NODE-extension.sql_id", + "data": { + "name": "dbms_scheduler" + }, + "store_object_id": true + }, + { + "type": "create", + "name": "Create Program with PLSQL_BLOCK", + "endpoint": "NODE-dbms_program.obj", + "sql_endpoint": "NODE-dbms_program.sql_id", + "msql_endpoint": "NODE-dbms_program.msql", + "data": { + "jsprid": null, + "jsprname": "dbms_prg_with_psql", + "jsprtype": "PLSQL_BLOCK", + "jsprenabled": true, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprdesc": "This is a PLSQL program.", + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;" + }, + "expected_sql_file": "create_program_psql.sql", + "expected_msql_file": "create_program_psql_msql.sql" + }, + { + "type": "delete", + "name": "Drop Program", + "endpoint": "NODE-dbms_program.obj_id", + "data": { + "name": "dbms_prg_with_psql" + } + }, + { + "type": "create", + "name": "Create Program with Stored Procedure without args", + "endpoint": "NODE-dbms_program.obj", + "sql_endpoint": "NODE-dbms_program.sql_id", + "msql_endpoint": "NODE-dbms_program.msql", + "data": { + "jsprid": null, + "jsprname": "dbms_prg_proc_without_args", + "jsprtype": "STORED_PROCEDURE", + "jsprenabled": false, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprdesc": "This is a STORED_PROCEDURE program.", + "jsprproc": "public.test_proc_without_args", + "jsprcode": null + }, + "expected_sql_file": "create_program_proc_without_args.sql", + "expected_msql_file": "create_program_proc_without_args_msql.sql" + }, + { + "type": "delete", + "name": "Drop Program", + "endpoint": "NODE-dbms_program.obj_id", + "data": { + "name": "dbms_prg_proc_without_args" + } + }, + { + "type": "create", + "name": "Create Program with Stored Procedure with args", + "endpoint": "NODE-dbms_program.obj", + "sql_endpoint": "NODE-dbms_program.sql_id", + "msql_endpoint": "NODE-dbms_program.msql", + "data": { + "jsprid": null, + "jsprname": "dbms_prg_proc_with_args", + "jsprtype": "STORED_PROCEDURE", + "jsprenabled": true, + "jsprnoofargs": 2, + "jsprarguments": [{"argid":0,"argtype":"bigint","argmode":"IN","argname":"salary","argdefval":"10000"},{"argid":1,"argtype":"character varying","argmode":"IN","argname":"name","argdefval":" -"}], + "jsprdesc": "This is a STORED_PROCEDURE program.", + "jsprproc": "public.test_proc_with_args", + "jsprcode": null + }, + "expected_sql_file": "create_program_proc_with_args.sql", + "expected_msql_file": "create_program_proc_with_args_msql.sql" + }, + { + "type": "delete", + "name": "Drop Program", + "endpoint": "NODE-dbms_program.obj_id", + "data": { + "name": "dbms_prg_proc_with_args" + } + }, + { + "type": "create", + "name": "Create disabled program", + "endpoint": "NODE-dbms_program.obj", + "sql_endpoint": "NODE-dbms_program.sql_id", + "msql_endpoint": "NODE-dbms_program.msql", + "data": { + "jsprid": null, + "jsprname": "dbms_prg_disabled", + "jsprtype": "PLSQL_BLOCK", + "jsprenabled": false, + "jsprnoofargs": 0, + "jsprarguments": [], + "jsprdesc": "", + "jsprproc": null, + "jsprcode": "BEGIN PERFORM 1; END;" + }, + "expected_sql_file": "create_program_disabled.sql", + "expected_msql_file": "create_program_disabled_msql.sql" + }, + { + "type": "delete", + "name": "Drop Program", + "endpoint": "NODE-dbms_program.obj_id", + "data": { + "name": "dbms_prg_disabled" + } + }, + { + "type": "delete", + "name": "Drop Extension", + "endpoint": "NODE-extension.delete", + "data": { + "ids": [""] + }, + "preprocess_data": true + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_add_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_add_program.py new file mode 100644 index 000000000..73ca2b964 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_add_program.py @@ -0,0 +1,83 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os +import json +from unittest.mock import patch +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSAddProgramTestCase(BaseTestGenerator): + """This class will test the add program in the DBMS Program API""" + scenarios = utils.generate_scenarios("dbms_create_program", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + def runTest(self): + """ This function will add DBMS Program under test database. """ + if self.is_positive_test: + response = job_scheduler_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + + # Verify in backend + response_data = json.loads(response.data) + self.programs_id = response_data['node']['_id'] + programs_name = response_data['node']['label'] + is_present = job_scheduler_utils.verify_dbms_program( + self, programs_name) + self.assertTrue( + is_present,"DBMS program was not created successfully.") + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=eval(self.mock_data["return_value"])): + response = job_scheduler_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_delete_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_delete_program.py new file mode 100644 index 000000000..19928dd00 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_delete_program.py @@ -0,0 +1,98 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSDeleteProgramTestCase(BaseTestGenerator): + """This class will test the add program in the DBMS program API""" + scenarios = utils.generate_scenarios("dbms_delete_program", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.prg_name = "test_program_delete%s" % str(uuid.uuid4())[1:8] + self.program_id = job_scheduler_utils.create_dbms_program( + self, self.prg_name) + + # multiple programs + if self.is_list: + self.prg_name2 = "test_program_delete%s" % str(uuid.uuid4())[1:8] + self.program_id_2 = job_scheduler_utils.create_dbms_program( + self, self.prg_name2) + + def runTest(self): + """ + This function will test delete DBMS program under test database. + """ + if self.is_list: + self.data['ids'] = [self.program_id, self.program_id_2] + response = job_scheduler_utils.api_delete(self, '') + + # Assert response + utils.assert_status_code(self, response) + + is_present = job_scheduler_utils.verify_dbms_program( + self, self.prg_name) + self.assertFalse( + is_present, "DBMS program was not deleted successfully") + + is_present = job_scheduler_utils.verify_dbms_program( + self, self.prg_name2) + self.assertFalse( + is_present, "DBMS program was not deleted successfully") + else: + response = job_scheduler_utils.api_delete(self) + + # Assert response + utils.assert_status_code(self, response) + + is_present = job_scheduler_utils.verify_dbms_program( + self, self.prg_name) + self.assertFalse( + is_present, "DBMS program was not deleted successfully") + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_disable_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_disable_program.py new file mode 100644 index 000000000..610dcbef2 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_disable_program.py @@ -0,0 +1,71 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSDisableProgramTestCase(BaseTestGenerator): + """This class will test the add program in the DBMS program API""" + scenarios = utils.generate_scenarios("dbms_disable_program", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.prg_name = "test_program_disable%s" % str(uuid.uuid4())[1:8] + self.data['program_name'] = self.prg_name + self.program_id = job_scheduler_utils.create_dbms_program( + self, self.prg_name) + + def runTest(self): + """ This function will test DBMS program under test database.""" + response = job_scheduler_utils.api_put(self, self.program_id) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.delete_dbms_program(self, self.prg_name) + + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_enable_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_enable_program.py new file mode 100644 index 000000000..9ce5b1762 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_enable_program.py @@ -0,0 +1,71 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSEnableProgramTestCase(BaseTestGenerator): + """This class will test the enable program in the DBMS program API""" + scenarios = utils.generate_scenarios("dbms_enable_program", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.prg_name = "test_program_enable%s" % str(uuid.uuid4())[1:8] + self.data['program_name'] = self.prg_name + self.program_id = job_scheduler_utils.create_dbms_program( + self, self.prg_name, False) + + def runTest(self): + """ This function will test DBMS program under test database.""" + response = job_scheduler_utils.api_put(self, self.program_id) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.delete_dbms_program(self, self.prg_name) + + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_msql_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_msql_program.py new file mode 100644 index 000000000..9c6a050c6 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_msql_program.py @@ -0,0 +1,65 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSGetMSQLProgramTestCase(BaseTestGenerator): + """This class will test the add program in the DBMS program API""" + scenarios = utils.generate_scenarios("dbms_msql_program", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + def runTest(self): + """ This function will add DBMS program under test database. """ + url_encode_data = self.data + + response = job_scheduler_utils.api_get_msql(self, url_encode_data) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_program.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_program.py new file mode 100644 index 000000000..76823d70b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/tests/test_dbms_get_program.py @@ -0,0 +1,92 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from unittest.mock import patch +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_programs_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSGetProgramTestCase(BaseTestGenerator): + """This class will test the add program in the DBMS program API""" + scenarios = utils.generate_scenarios("dbms_get_program", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.prg_name = "test_program_get%s" % str(uuid.uuid4())[1:8] + self.program_id = job_scheduler_utils.create_dbms_program( + self, self.prg_name) + + # multiple programs + if self.is_list: + self.prg_name2 = "test_program_get%s" % str(uuid.uuid4())[1:8] + self.program_id_2 = job_scheduler_utils.create_dbms_program( + self, self.prg_name2, True, True, self.proc_name) + + def runTest(self): + """ This function will test DBMS program under test database.""" + if self.is_positive_test: + if self.is_list: + response = job_scheduler_utils.api_get(self, '') + else: + response = job_scheduler_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=[eval(self.mock_data["return_value"])]): + response = job_scheduler_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.delete_dbms_program(self, self.prg_name) + if self.is_list: + job_scheduler_utils.delete_dbms_program(self, self.prg_name2) + + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/__init__.py new file mode 100644 index 000000000..00be42333 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/__init__.py @@ -0,0 +1,508 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" Implements DBMS Schedule objects Node.""" + +import json +from functools import wraps + +from flask import render_template, request, jsonify +from flask_babel import gettext +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.server_groups.servers import databases +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, gone, \ + make_response as ajax_response, internal_server_error +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID +from pgadmin.browser.server_groups.servers.databases.dbms_job_scheduler.utils \ + import resolve_calendar_string, create_calendar_string + + +class DBMSScheduleModule(CollectionNodeModule): + """ + class DBMSScheduleModule(CollectionNodeModule) + + A module class for DBMS Schedule objects node derived + from CollectionNodeModule. + + Methods: + ------- + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * script_load() + - Load the module script for DBMS Schedule objects, when any of + the server node is initialized. + """ + _NODE_TYPE = 'dbms_schedule' + _COLLECTION_LABEL = gettext("DBMS Schedules") + + @property + def collection_icon(self): + """ + icon to be displayed for the browser collection node + """ + return 'icon-coll-pga_schedule' + + @property + def node_icon(self): + """ + icon to be displayed for the browser nodes + """ + return 'icon-pga_schedule' + + def get_nodes(self, gid, sid, did, jsid): + """ + Generate the collection node + """ + if self.show_node: + yield self.generate_browser_collection_node(did) + + @property + def node_inode(self): + """ + Override this property to make the node a leaf node. + + Returns: False as this is the leaf node + """ + return False + + @property + def script_load(self): + """ + Load the module script for server, when any of the database node is + initialized. + """ + return databases.DatabaseModule.node_type + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + +blueprint = DBMSScheduleModule(__name__) + + +class DBMSScheduleView(PGChildNodeView): + """ + class DBMSScheduleView(PGChildNodeView) + + A view class for DBMSSchedule node derived from PGChildNodeView. + This class is responsible for all the stuff related to view like + updating schedule node, showing properties, showing sql in sql pane. + + Methods: + ------- + * __init__(**kwargs) + - Method is used to initialize the DBMSScheduleView, and it's base view. + + * check_precondition() + - This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + + * list() + - This function is used to list all the schedule nodes within that + collection. + + * nodes() + - This function will use to create all the child node within that + collection. Here it will create all the schedule node. + + * properties(gid, sid, did, jsid, jsscid) + - This function will show the properties of the selected schedule node + + * create(gid, sid, did, jsid, jsscid) + - This function will create the new schedule object + + * msql(gid, sid, did, jsid, jsscid) + - This function is used to return modified SQL for the + selected schedule node + + * sql(gid, sid, did, jsid, jsscid) + - Dummy response for sql panel + + * delete(gid, sid, did, jsid, jsscid) + - Drops job schedule + """ + + node_type = blueprint.node_type + BASE_TEMPLATE_PATH = 'dbms_schedules/ppas/#{0}#' + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'did'}, + {'type': 'int', 'id': 'jsid'} + ] + ids = [ + {'type': 'int', 'id': 'jsscid'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties', 'delete': 'delete'}, + {'get': 'list', 'post': 'create', 'delete': 'delete'} + ], + 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}], + 'msql': [{'get': 'msql'}, {'get': 'msql'}], + 'sql': [{'get': 'sql'}] + }) + + def _init_(self, **kwargs): + self.conn = None + self.template_path = None + self.manager = None + + super().__init__(**kwargs) + + def check_precondition(f): + """ + This function will behave as a decorator which will check the + database connection before running view. It will also attach + manager, conn & template_path properties to self + """ + + @wraps(f) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = self.driver.connection_manager(kwargs['sid']) + self.conn = self.manager.connection(did=kwargs['did']) + # Set the template path for the SQL scripts + self.template_path = self.BASE_TEMPLATE_PATH.format( + self.manager.version) + + return f(*args, **kwargs) + + return wrap + + @check_precondition + def list(self, gid, sid, did, jsid): + """ + This function is used to list all the schedule nodes within + that collection. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL])) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition + def nodes(self, gid, sid, did, jsid, jsscid=None): + """ + This function is used to create all the child nodes within + the collection. Here it will create all the schedule nodes. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + res = [] + try: + sql = render_template( + "/".join([self.template_path, self._NODES_SQL])) + + status, result = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=result) + + if jsscid is not None: + if len(result['rows']) == 0: + return gone( + errormsg=gettext("Could not find the specified " + "schedule.") + ) + + row = result['rows'][0] + return make_json_response( + data=self.blueprint.generate_browser_node( + row['jsscid'], + DBMS_JOB_SCHEDULER_ID, + row['jsscname'], + icon="icon-pga_schedule", + description=row['jsscdesc'] + ) + ) + + for row in result['rows']: + res.append( + self.blueprint.generate_browser_node( + row['jsscid'], + DBMS_JOB_SCHEDULER_ID, + row['jsscname'], + icon="icon-pga_schedule", + description=row['jsscdesc'] + ) + ) + + return make_json_response( + data=res, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def properties(self, gid, sid, did, jsid, jsscid): + """ + This function will show the properties of the selected schedule node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + jsscid: JobSchedule ID + """ + try: + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + jsscid=jsscid + ) + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone( + errormsg=gettext("Could not find the specified schedule.") + ) + + # Resolve the repeat interval string + if 'jsscrepeatint' in res['rows'][0]: + (freq, by_date, by_month, by_month_day, by_weekday, by_hour, + by_minute) = resolve_calendar_string( + res['rows'][0]['jsscrepeatint']) + + res['rows'][0]['jsscfreq'] = freq + res['rows'][0]['jsscdate'] = by_date + res['rows'][0]['jsscmonths'] = by_month + res['rows'][0]['jsscmonthdays'] = by_month_day + res['rows'][0]['jsscweekdays'] = by_weekday + res['rows'][0]['jsschours'] = by_hour + res['rows'][0]['jsscminutes'] = by_minute + + return ajax_response( + response=res['rows'][0], + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def create(self, gid, sid, did, jsid): + """ + This function will create the schedule node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + """ + data = json.loads(request.data) + try: + # Create calendar string for repeat interval + repeat_interval = create_calendar_string( + data['jsscfreq'], data['jsscdate'], data['jsscmonths'], + data['jsscmonthdays'], data['jsscweekdays'], data['jsschours'], + data['jsscminutes']) + + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + schedule_name=data['jsscname'], + start_date=data['jsscstart'], + repeat_interval=repeat_interval, + end_date=data['jsscend'], + comments=data['jsscdesc'], + conn=self.conn + ) + + status, res = self.conn.execute_void('BEGIN') + if not status: + return internal_server_error(errormsg=res) + + status, res = self.conn.execute_scalar(sql) + if not status: + if self.conn.connected(): + self.conn.execute_void('END') + return internal_server_error(errormsg=res) + + self.conn.execute_void('END') + + # Get the newly created Schedule id + sql = render_template( + "/".join([self.template_path, 'get_schedule_id.sql']), + jsscname=data['jsscname'], conn=self.conn + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone( + errormsg=gettext("Job schedule creation failed.") + ) + row = res['rows'][0] + + return jsonify( + node=self.blueprint.generate_browser_node( + row['jsscid'], + DBMS_JOB_SCHEDULER_ID, + row['jsscname'], + icon="icon-pga_schedule" + ) + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def delete(self, gid, sid, did, jsid, jsscid=None): + """Delete the Job Schedule.""" + + if jsscid is None: + data = request.form if request.form else json.loads( + request.data + ) + else: + data = {'ids': [jsscid]} + + try: + for jsscid in data['ids']: + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + jsscid=jsscid + ) + + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + jsscname = res['rows'][0]['jsscname'] + + status, res = self.conn.execute_void( + render_template( + "/".join([self.template_path, self._DELETE_SQL]), + schedule_name=jsscname, force=False, conn=self.conn + ) + ) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response(success=1) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def msql(self, gid, sid, did, jsid, jsscid=None): + """ + This function is used to return modified SQL for the + selected Schedule node. + + Args: + gid: Server Group ID + sid: Server ID + jsid: Job Scheduler ID + jsscid: Job Schedule ID (optional) + """ + data = {} + for k, v in request.args.items(): + try: + # 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 ('jsscdesc',): + data[k] = v + else: + data[k] = json.loads(v) + except ValueError: + data[k] = v + + try: + # Create calendar string for repeat interval + repeat_interval = create_calendar_string( + data['jsscfreq'], data['jsscdate'], data['jsscmonths'], + data['jsscmonthdays'], data['jsscweekdays'], data['jsschours'], + data['jsscminutes']) + + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + schedule_name=data['jsscname'], + start_date=data['jsscstart'], + repeat_interval=repeat_interval, + end_date=data['jsscend'], + comments=data['jsscdesc'], + conn=self.conn + ) + + return make_json_response( + data=sql, + status=200 + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition + def sql(self, gid, sid, did, jsid, jsscid): + """ + This function will generate sql for the sql panel + """ + try: + SQL = render_template("/".join( + [self.template_path, self._PROPERTIES_SQL] + ), jsscid=jsscid) + + status, res = self.conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=res) + + if len(res['rows']) == 0: + return gone( + gettext("Could not find the DBMS Schedule.") + ) + + data = res['rows'][0] + + SQL = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + display_comments=True, + schedule_name=data['jsscname'], + start_date=data['jsscstart'], + repeat_interval=data['jsscrepeatint'], + end_date=data['jsscend'], + comments=data['jsscdesc'], + conn=self.conn + ) + + return ajax_response(response=SQL) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + +DBMSScheduleView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.js new file mode 100644 index 000000000..c8ec3c70c --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.js @@ -0,0 +1,77 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import DBMSScheduleSchema from './dbms_schedule.ui'; + +define('pgadmin.node.dbms_schedule', [ + 'sources/gettext', 'sources/url_for', 'sources/pgadmin', + 'pgadmin.browser', 'pgadmin.browser.collection', +], function(gettext, url_for, pgAdmin, pgBrowser) { + + if (!pgBrowser.Nodes['coll-dbms_schedule']) { + pgBrowser.Nodes['coll-dbms_schedule'] = + pgBrowser.Collection.extend({ + node: 'dbms_schedule', + label: gettext('DBMS Schedules'), + type: 'coll-dbms_schedule', + columns: ['jsscname', 'jsscrepeatint', 'jsscdesc'], + hasSQL: false, + hasDepends: false, + hasStatistics: false, + hasScriptTypes: [], + canDrop: true, + canDropCascade: false, + }); + } + + if (!pgBrowser.Nodes['dbms_schedule']) { + pgAdmin.Browser.Nodes['dbms_schedule'] = pgAdmin.Browser.Node.extend({ + parent_type: 'dbms_job_scheduler', + type: 'dbms_schedule', + label: gettext('DBMS Schedule'), + node_image: 'icon-pga_schedule', + epasHelp: true, + epasURL: 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_bip_guide/03_built-in_packages/15_dbms_scheduler/04_create_schedule/', + dialogHelp: url_for('help.static', {'filename': 'dbms_schedule.html'}), + canDrop: true, + hasSQL: true, + hasDepends: false, + hasStatistics: false, + Init: function() { + /* Avoid multiple registration of menus */ + if (this.initialized) + return; + + this.initialized = true; + + pgBrowser.add_menus([{ + name: 'create_dbms_schedule_on_coll', node: 'coll-dbms_schedule', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Schedule...'), + data: {action: 'create'}, + },{ + name: 'create_dbms_schedule', node: 'dbms_schedule', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Schedule...'), + data: {action: 'create'}, + },{ + name: 'create_dbms_schedule', node: 'dbms_job_scheduler', module: this, + applies: ['object', 'context'], callback: 'show_obj_properties', + category: 'create', priority: 4, label: gettext('DBMS Schedule...'), + data: {action: 'create'}, + }, + ]); + }, + getSchema: ()=>new DBMSScheduleSchema(), + + }); + } + + return pgBrowser.Nodes['dbms_schedule']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.ui.js new file mode 100644 index 000000000..9872c2525 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule.ui.js @@ -0,0 +1,89 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { isEmptyString } from 'sources/validators'; +import moment from 'moment'; +import { getRepeatSchema } from '../../../static/js/dbms_job_scheduler_common.ui'; + +export default class DBMSScheduleSchema extends BaseUISchema { + constructor() { + super({ + jsscid: null, + jsscname: '', + jsscstart: null, + jsscend: null, + jsscdesc: '', + jsscrepeatint: '', + jsscfreq: null, + jsscdate: null, + jsscweekdays: null, + jsscmonthdays: null, + jsscmonths: null, + jsschours: null, + jsscminutes: null, + }); + } + + get idAttribute() { + return 'jsscid'; + } + + get baseFields() { + let obj = this; + return [ + { + id: 'jsscid', label: gettext('ID'), type: 'int', mode: ['properties'], + readonly: function(state) {return !obj.isNew(state); }, + }, { + id: 'jsscname', label: gettext('Name'), cell: 'text', + editable: false, type: 'text', noEmpty: true, + readonly: function(state) {return !obj.isNew(state); }, + }, + // Add the Repeat Schema. + ...getRepeatSchema(obj, 'schedule'), + { + id: 'jsscdesc', label: gettext('Comment'), type: 'multiline', + readonly: function(state) {return !obj.isNew(state); }, + }, + ]; + } + + validate(state, setError) { + if (isEmptyString(state.jsscstart) && isEmptyString(state.jsscfreq) && + isEmptyString(state.jsscmonths) && isEmptyString(state.jsscweekdays) && + isEmptyString(state.jsscmonthdays) && isEmptyString(state.jsschours) && + isEmptyString(state.jsscminutes) && isEmptyString(state.jsscdate)) { + setError('jsscstart', gettext('Either Start time or Repeat interval must be specified.')); + return true; + } else { + setError('jsscstart', null); + } + + if (!isEmptyString(state.jsscend)) { + let start_time = state.jsscstart, + end_time = state.jsscend, + start_time_js = start_time.split(' '), + end_time_js = end_time.split(' '); + + start_time_js = moment(start_time_js[0] + ' ' + start_time_js[1]); + end_time_js = moment(end_time_js[0] + ' ' + end_time_js[1]); + + if(end_time_js.isBefore(start_time_js)) { + setError('jsscend', gettext('Start time must be less than end time')); + return true; + } else { + setError('jsscend', null); + } + } else { + state.jsscend = null; + } + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/create.sql new file mode 100644 index 000000000..aebd77cc4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/create.sql @@ -0,0 +1,22 @@ +{% if display_comments %} +-- DBMS Schedule: '{{ schedule_name }}' + +-- EXEC dbms_scheduler.DROP_SCHEDULE('{{ schedule_name }}'); + +{% endif %} +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => {{ schedule_name|qtLiteral(conn) }}, + repeat_interval => {{ repeat_interval|qtLiteral(conn) }}{% if start_date or end_date or comments %},{% endif %} +{% if start_date %} + + start_date => {{ start_date|qtLiteral(conn) }}{% if end_date or comments %},{% endif %} +{% endif %} +{% if end_date %} + + end_date => {{ end_date|qtLiteral(conn) }}{% if comments %},{% endif %} +{% endif %} +{% if comments %} + + comments => {{ comments|qtLiteral(conn) }} +{% endif %} +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/delete.sql new file mode 100644 index 000000000..dae6dbfd9 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/delete.sql @@ -0,0 +1,6 @@ +EXEC dbms_scheduler.DROP_SCHEDULE( + {{ schedule_name|qtLiteral(conn) }}{% if force %},{% endif %} +{% if force %} + {{ force }} +{% endif %} +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/get_schedule_id.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/get_schedule_id.sql new file mode 100644 index 000000000..a7794be3a --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/get_schedule_id.sql @@ -0,0 +1,4 @@ +SELECT + dss_schedule_id as jsscid, dss_schedule_name as jsscname +FROM sys.scheduler_0300_schedule +WHERE dss_schedule_name={{ jsscname|qtLiteral(conn) }} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/nodes.sql new file mode 100644 index 000000000..9313c96de --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/nodes.sql @@ -0,0 +1,5 @@ +SELECT + dss_schedule_id as jsscid, dss_schedule_name as jsscname, + dss_comments as jsscdesc +FROM sys.scheduler_0300_schedule sct + JOIN sys.dba_scheduler_schedules scv ON sct.dss_schedule_name = scv.schedule_name; diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/properties.sql new file mode 100644 index 000000000..ca8ea8202 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/templates/dbms_schedules/ppas/16_plus/properties.sql @@ -0,0 +1,12 @@ +SELECT + dss_schedule_id as jsscid, dss_schedule_name as jsscname, + dss_start_date as jsscstart, dss_end_date as jsscend, + dss_repeat_interval as jsscrepeatint, dss_comments as jsscdesc +FROM sys.scheduler_0300_schedule sct +{% if not jsscid %} + JOIN sys.dba_scheduler_schedules scv ON sct.dss_schedule_name = scv.schedule_name +{% endif %} +{% if jsscid %} +WHERE dss_schedule_id={{jsscid}}::oid +{% endif %} +ORDER BY dss_schedule_name diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/dbms_schedules_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/dbms_schedules_test_data.json new file mode 100644 index 000000000..d216ef160 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/dbms_schedules_test_data.json @@ -0,0 +1,335 @@ +{ + "dbms_create_schedule": [ + { + "name": "Create schedule: YEARLY", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_yearly", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is yearly test schedule", + "jsscrepeatint": "", + "jsscfreq": "YEARLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create schedule: YEARLY BY DATE", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_yearly_by_date", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is yearly by date test schedule", + "jsscrepeatint": "", + "jsscfreq": "YEARLY", + "jsscdate": "20250113", + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create schedule: MONTHLY", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_monthly", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is monthly test schedule", + "jsscrepeatint": "", + "jsscfreq": "MONTHLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create schedule: WEEKLY", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_weekly", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is weekly test schedule", + "jsscrepeatint": "", + "jsscfreq": "WEEKLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create schedule: DAILY", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_daily", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is daily test schedule", + "jsscrepeatint": "", + "jsscfreq": "DAILY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create schedule: HOURLY", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_hourly", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is hourly test schedule", + "jsscrepeatint": "", + "jsscfreq": "HOURLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create schedule: MINUTELY", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_minutely", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is minutely test schedule", + "jsscrepeatint": "", + "jsscfreq": "MINUTELY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create schedule: while server is down", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": false, + "test_data": { + "jsscid": null, + "jsscname": "dbms_test_sch_yearly", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is yearly test schedule", + "jsscrepeatint": "", + "jsscfreq": "YEARLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_scalar", + "return_value": "[(False,'Mocked Internal Server Error')]" + }, + "expected_data": { + "status_code": 500, + "error_msg": "Mocked Internal Server Error", + "test_result_data": {} + } + } + ], + "dbms_delete_schedule": [ + { + "name": "Delete schedule: With existing DBMS schedule.", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Delete multiple schedules: With existing DBMS schedule.", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": true + } + ], + "dbms_get_schedule": [ + { + "name": "Get schedule: With existing DBMS schedule.", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + }, + { + "name": "Get schedules: With multiple existing DBMS schedules.", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": true + }, + { + "name": "Get schedule: while server down.", + "url": "/browser/dbms_schedule/obj/", + "is_positive_test": false, + "inventory_data": {}, + "test_data": {}, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg3.connection.Connection.execute_dict", + "return_value": "(False,'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500, + "error_msg": "Mocked Internal Server Error", + "test_result_data": {} + }, + "is_list": false + } + ], + "dbms_msql_schedule": [ + { + "name": "Get schedule msql: For existing dbms schedule.", + "url": "/browser/dbms_schedule/msql/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": { + "jsscname": "dbms_test_sch_msql", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is daily test schedule", + "jsscrepeatint": "", + "jsscfreq": "DAILY", + "jsscdate": null, + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + }, + "is_list": false + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all.sql new file mode 100644 index 000000000..43c420216 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all.sql @@ -0,0 +1,11 @@ +-- DBMS Schedule: 'dbms_test_sch_yearly' + +-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_yearly'); + +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_yearly', + repeat_interval => 'FREQ=YEARLY;BYMONTH=JAN,MAY,DEC;BYMONTHDAY=2,8,31,27;BYDAY=SUN,MON,TUE,WED,THU,FRI,SAT;BYHOUR=05,18,22;BYMINUTE=45,37,58', + start_date => '2024-02-27 00:00:00+05:30', + end_date => '2024-02-28 00:00:00+05:30', + comments => 'This is yearly test schedule' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all_msql.sql new file mode 100644 index 000000000..8c615d920 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_all_msql.sql @@ -0,0 +1,7 @@ +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_yearly', + repeat_interval => 'FREQ=YEARLY;BYMONTH=JAN,MAY,DEC;BYMONTHDAY=2,8,31,27;BYDAY=SUN,MON,TUE,WED,THU,FRI,SAT;BYHOUR=05,18,22;BYMINUTE=45,37,58', + start_date => '2024-02-27 00:00:00 +05:30', + end_date => '2024-02-28 00:00:00 +05:30', + comments => 'This is yearly test schedule' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate.sql new file mode 100644 index 000000000..7c7d7eaa2 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate.sql @@ -0,0 +1,11 @@ +-- DBMS Schedule: 'dbms_test_sch_yearly_by_date' + +-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_yearly_by_date'); + +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_yearly_by_date', + repeat_interval => 'FREQ=YEARLY;BYDATE=20250113;', + start_date => '2024-02-27 00:00:00+05:30', + end_date => '2024-02-28 00:00:00+05:30', + comments => 'This is yearly by date test schedule' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate_msql.sql new file mode 100644 index 000000000..332db2451 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_bydate_msql.sql @@ -0,0 +1,7 @@ +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_yearly_by_date', + repeat_interval => 'FREQ=YEARLY;BYDATE=20250113;', + start_date => '2024-02-27 00:00:00 +05:30', + end_date => '2024-02-28 00:00:00 +05:30', + comments => 'This is yearly by date test schedule' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq.sql new file mode 100644 index 000000000..029edffaa --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq.sql @@ -0,0 +1,7 @@ +-- DBMS Schedule: 'dbms_test_sch_daily_freq' + +-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_daily_freq'); + +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_daily_freq', + repeat_interval => 'FREQ=DAILY;'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm.sql new file mode 100644 index 000000000..b6f2c1415 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm.sql @@ -0,0 +1,9 @@ +-- DBMS Schedule: 'dbms_test_sch_weekly_comm' + +-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_weekly_comm'); + +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_weekly_comm', + repeat_interval => 'FREQ=WEEKLY;', + comments => 'This is weekly test schedule' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm_msql.sql new file mode 100644 index 000000000..69c03dc7d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_comm_msql.sql @@ -0,0 +1,5 @@ +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_weekly_comm', + repeat_interval => 'FREQ=WEEKLY;', + comments => 'This is weekly test schedule' +); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_msql.sql new file mode 100644 index 000000000..e6d25e32d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_freq_msql.sql @@ -0,0 +1,3 @@ +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_daily_freq', + repeat_interval => 'FREQ=DAILY;'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date.sql new file mode 100644 index 000000000..f989e273a --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date.sql @@ -0,0 +1,8 @@ +-- DBMS Schedule: 'dbms_test_sch_monthly_start_date' + +-- EXEC dbms_scheduler.DROP_SCHEDULE('dbms_test_sch_monthly_start_date'); + +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_monthly_start_date', + repeat_interval => 'FREQ=MONTHLY;', + start_date => '2024-02-27 00:00:00+05:30'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date_msql.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date_msql.sql new file mode 100644 index 000000000..d115325f8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/create_schedule_start_date_msql.sql @@ -0,0 +1,4 @@ +EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => 'dbms_test_sch_monthly_start_date', + repeat_interval => 'FREQ=MONTHLY;', + start_date => '2024-02-27 00:00:00 +05:30'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/test.json b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/test.json new file mode 100644 index 000000000..37f861348 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/ppas/16_plus/test.json @@ -0,0 +1,188 @@ +{ + "scenarios": [ + { + "type": "create", + "name": "Create extension 'edb_job_scheduler' for DBMS Schedule", + "endpoint": "NODE-extension.obj", + "sql_endpoint": "NODE-extension.sql_id", + "data": { + "name": "edb_job_scheduler" + }, + "store_object_id": true + }, + { + "type": "create", + "name": "Create extension 'dbms_scheduler' for DBMS Schedule", + "endpoint": "NODE-extension.obj", + "sql_endpoint": "NODE-extension.sql_id", + "data": { + "name": "dbms_scheduler" + }, + "store_object_id": true + }, + { + "type": "create", + "name": "Create Schedule with all options", + "endpoint": "NODE-dbms_schedule.obj", + "sql_endpoint": "NODE-dbms_schedule.sql_id", + "msql_endpoint": "NODE-dbms_schedule.msql", + "data": { + "jsscname": "dbms_test_sch_yearly", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is yearly test schedule", + "jsscrepeatint": "", + "jsscfreq": "YEARLY", + "jsscdate": null, + "jsscweekdays": ["7", "1", "2", "3", "4", "5", "6"], + "jsscmonthdays": ["2", "8", "31", "27"], + "jsscmonths": ["1", "5", "12"], + "jsschours": ["05", "18", "22"], + "jsscminutes": ["45", "37", "58"] + }, + "expected_sql_file": "create_schedule_all.sql", + "expected_msql_file": "create_schedule_all_msql.sql" + }, + { + "type": "delete", + "name": "Drop Schedule", + "endpoint": "NODE-dbms_schedule.obj_id", + "data": { + "name": "dbms_test_sch_yearly" + } + }, + { + "type": "create", + "name": "Create Schedule Yearly by date", + "endpoint": "NODE-dbms_schedule.obj", + "sql_endpoint": "NODE-dbms_schedule.sql_id", + "msql_endpoint": "NODE-dbms_schedule.msql", + "data": { + "jsscname": "dbms_test_sch_yearly_by_date", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "2024-02-28 00:00:00 +05:30", + "jsscdesc": "This is yearly by date test schedule", + "jsscrepeatint": "", + "jsscfreq": "YEARLY", + "jsscdate": "20250113", + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [] + }, + "expected_sql_file": "create_schedule_bydate.sql", + "expected_msql_file": "create_schedule_bydate_msql.sql" + }, + { + "type": "delete", + "name": "Drop Schedule", + "endpoint": "NODE-dbms_schedule.obj_id", + "data": { + "name": "dbms_test_sch_yearly_by_date" + } + }, + { + "type": "create", + "name": "Create Schedule only start date", + "endpoint": "NODE-dbms_schedule.obj", + "sql_endpoint": "NODE-dbms_schedule.sql_id", + "msql_endpoint": "NODE-dbms_schedule.msql", + "data": { + "jsscname": "dbms_test_sch_monthly_start_date", + "jsscstart": "2024-02-27 00:00:00 +05:30", + "jsscend": "", + "jsscdesc": "", + "jsscrepeatint": "", + "jsscfreq": "MONTHLY", + "jsscdate": null, + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [] + }, + "expected_sql_file": "create_schedule_start_date.sql", + "expected_msql_file": "create_schedule_start_date_msql.sql" + }, + { + "type": "delete", + "name": "Drop Schedule", + "endpoint": "NODE-dbms_schedule.obj_id", + "data": { + "name": "dbms_test_sch_monthly_start_date" + } + }, + { + "type": "create", + "name": "Create Schedule only frequency", + "endpoint": "NODE-dbms_schedule.obj", + "sql_endpoint": "NODE-dbms_schedule.sql_id", + "msql_endpoint": "NODE-dbms_schedule.msql", + "data": { + "jsscname": "dbms_test_sch_daily_freq", + "jsscstart": "", + "jsscend": "", + "jsscdesc": "", + "jsscrepeatint": "", + "jsscfreq": "DAILY", + "jsscdate": null, + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [] + }, + "expected_sql_file": "create_schedule_freq.sql", + "expected_msql_file": "create_schedule_freq_msql.sql" + }, + { + "type": "delete", + "name": "Drop Schedule", + "endpoint": "NODE-dbms_schedule.obj_id", + "data": { + "name": "dbms_test_sch_daily_freq" + } + }, + { + "type": "create", + "name": "Create Schedule frequency with comment", + "endpoint": "NODE-dbms_schedule.obj", + "sql_endpoint": "NODE-dbms_schedule.sql_id", + "msql_endpoint": "NODE-dbms_schedule.msql", + "data": { + "jsscname": "dbms_test_sch_weekly_comm", + "jsscstart": "", + "jsscend": "", + "jsscdesc": "This is weekly test schedule", + "jsscrepeatint": "", + "jsscfreq": "WEEKLY", + "jsscdate": null, + "jsscweekdays": [], + "jsscmonthdays": [], + "jsscmonths": [], + "jsschours": [], + "jsscminutes": [] + }, + "expected_sql_file": "create_schedule_freq_comm.sql", + "expected_msql_file": "create_schedule_freq_comm_msql.sql" + }, + { + "type": "delete", + "name": "Drop Schedule", + "endpoint": "NODE-dbms_schedule.obj_id", + "data": { + "name": "dbms_test_sch_weekly_comm" + } + }, + { + "type": "delete", + "name": "Drop Extension", + "endpoint": "NODE-extension.delete", + "data": { + "ids": [""] + }, + "preprocess_data": true + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_add_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_add_schedule.py new file mode 100644 index 000000000..c08e468c4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_add_schedule.py @@ -0,0 +1,83 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os +import json +from unittest.mock import patch +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSAddScheduleTestCase(BaseTestGenerator): + """This class will test the add schedule in the DBMS Schedule API""" + scenarios = utils.generate_scenarios("dbms_create_schedule", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + def runTest(self): + """ This function will add DBMS Schedule under test database. """ + if self.is_positive_test: + response = job_scheduler_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + + # Verify in backend + response_data = json.loads(response.data) + self.schedule_id = response_data['node']['_id'] + schedule_name = response_data['node']['label'] + is_present = job_scheduler_utils.verify_dbms_schedule( + self, schedule_name) + self.assertTrue( + is_present,"DBMS schedule was not created successfully.") + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=eval(self.mock_data["return_value"])): + response = job_scheduler_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_delete_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_delete_schedule.py new file mode 100644 index 000000000..77bd31851 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_delete_schedule.py @@ -0,0 +1,98 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSDeleteScheduleTestCase(BaseTestGenerator): + """This class will test the add schedule in the DBMS Schedule API""" + scenarios = utils.generate_scenarios("dbms_delete_schedule", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.sch_name = "test_schedule_delete%s" % str(uuid.uuid4())[1:8] + self.schedule_id = job_scheduler_utils.create_dbms_schedule( + self, self.sch_name) + + # multiple schedules + if self.is_list: + self.sch_name2 = "test_schedule_delete%s" % str(uuid.uuid4())[1:8] + self.schedule_id_2 = job_scheduler_utils.create_dbms_schedule( + self, self.sch_name2) + + def runTest(self): + """ + This function will test delete DBMS Schedule under test database. + """ + if self.is_list: + self.data['ids'] = [self.schedule_id, self.schedule_id_2] + response = job_scheduler_utils.api_delete(self, '') + + # Assert response + utils.assert_status_code(self, response) + + is_present = job_scheduler_utils.verify_dbms_schedule( + self, self.sch_name) + self.assertFalse( + is_present, "DBMS schedule was not deleted successfully") + + is_present = job_scheduler_utils.verify_dbms_schedule( + self, self.sch_name2) + self.assertFalse( + is_present, "DBMS schedule was not deleted successfully") + else: + response = job_scheduler_utils.api_delete(self) + + # Assert response + utils.assert_status_code(self, response) + + is_present = job_scheduler_utils.verify_dbms_schedule( + self, self.sch_name) + self.assertFalse( + is_present, "DBMS schedule was not deleted successfully") + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_msql_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_msql_schedule.py new file mode 100644 index 000000000..c3cd8db83 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_msql_schedule.py @@ -0,0 +1,65 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import os +import json +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSGetMSQLScheduleTestCase(BaseTestGenerator): + """This class will test the add schedule in the DBMS Schedule API""" + scenarios = utils.generate_scenarios("dbms_msql_schedule", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + def runTest(self): + """ This function will add DBMS Schedule under test database. """ + url_encode_data = self.data + + response = job_scheduler_utils.api_get_msql(self, url_encode_data) + + # Assert response + utils.assert_status_code(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_schedule.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_schedule.py new file mode 100644 index 000000000..9a996e58e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/tests/test_dbms_get_schedule.py @@ -0,0 +1,92 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import os +import json +from unittest.mock import patch +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from ...tests import utils as job_scheduler_utils +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + + +# Load test data from json file. +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/dbms_schedules_test_data.json") as data_file: + test_cases = json.load(data_file) + + +class DBMSGetScheduleTestCase(BaseTestGenerator): + """This class will test the add schedule in the DBMS Schedule API""" + scenarios = utils.generate_scenarios("dbms_get_schedule", + test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + if not job_scheduler_utils.is_supported_version(self): + self.skipTest(job_scheduler_utils.SKIP_MSG) + + # Create db + self.db_name, self.db_id = job_scheduler_utils.create_test_database( + self) + db_con = database_utils.connect_database(self, + utils.SERVER_GROUP, + self.server_id, + self.db_id) + if db_con["info"] != "Database connected.": + raise Exception("Could not connect to database.") + + # Create extension required for job scheduler + job_scheduler_utils.create_job_scheduler_extensions(self) + + if not job_scheduler_utils.is_dbms_job_scheduler_present(self): + self.skipTest(job_scheduler_utils.SKIP_MSG_EXTENSION) + + self.sch_name = "test_schedule_get%s" % str(uuid.uuid4())[1:8] + self.schedule_id = job_scheduler_utils.create_dbms_schedule( + self, self.sch_name) + + # multiple schedules + if self.is_list: + self.sch_name2 = "test_schedule_get%s" % str(uuid.uuid4())[1:8] + self.schedule_id_2 = job_scheduler_utils.create_dbms_schedule( + self, self.sch_name2) + + def runTest(self): + """ This function will test DBMS Schedule under test database.""" + if self.is_positive_test: + if self.is_list: + response = job_scheduler_utils.api_get(self, '') + else: + response = job_scheduler_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=[eval(self.mock_data["return_value"])]): + response = job_scheduler_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + """This function will do the cleanup task.""" + job_scheduler_utils.delete_dbms_schedule(self, self.sch_name) + if self.is_list: + job_scheduler_utils.delete_dbms_schedule(self, self.sch_name2) + + job_scheduler_utils.clean_up(self) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/img/coll-dbms_job_scheduler.svg b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/img/coll-dbms_job_scheduler.svg new file mode 100644 index 000000000..848254661 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/img/coll-dbms_job_scheduler.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler.js new file mode 100644 index 000000000..db52b9173 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler.js @@ -0,0 +1,47 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import DBMSJobSchedulerSchema from './dbms_jobscheduler.ui'; + +define('pgadmin.node.dbms_job_scheduler', [ + 'sources/gettext', 'sources/pgadmin', + 'pgadmin.browser', 'pgadmin.browser.collection', 'pgadmin.node.dbms_job', + 'pgadmin.node.dbms_program', 'pgadmin.node.dbms_schedule', +], function(gettext, pgAdmin, pgBrowser) { + + + if (!pgBrowser.Nodes['dbms_job_scheduler']) { + pgAdmin.Browser.Nodes['dbms_job_scheduler'] = pgAdmin.Browser.Node.extend({ + parent_type: 'database', + type: 'dbms_job_scheduler', + label: gettext('DBMS Job Scheduler'), + columns: ['jobname', 'jobstatus', 'joberror', 'jobstarttime', 'jobendtime', 'jobnextrun'], + canDrop: false, + canDropCascade: false, + canSelect: false, + hasSQL: false, + hasDepends: false, + hasStatistics: false, + hasScriptTypes: [], + Init: function() { + /* Avoid multiple registration of menus */ + if (this.initialized) + return; + + this.initialized = true; + + }, + getSchema: ()=> { + return new DBMSJobSchedulerSchema(); + } + }); + } + + return pgBrowser.Nodes['dbms_job_scheduler']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler_common.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler_common.ui.js new file mode 100644 index 000000000..192ccb697 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler_common.ui.js @@ -0,0 +1,255 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { WEEKDAYS, MONTHDAYS, MONTHS, HOURS, MINUTES } from '../../../../../../static/js/constants'; + +function isReadOnly(obj, state, parentName) { + // Always true in case of edit dialog + if (!obj.isNew(state)) + return true; + + // Check the parent name and based on the condition return appropriate value. + if (parentName == 'job') { + return obj.isNew(state) && state.jsjobtype == 'p'; + } + return false; +} + + +export function getRepeatSchema(obj, parentName) { + return [ + { + id: 'jsscrepeatint', label: gettext('Repeat Interval'), cell: 'text', + readonly: true, type: 'multiline', mode: ['edit', 'properties'], + group: gettext('Repeat'), + }, { + id: 'jsscstart', label: gettext('Start'), type: 'datetimepicker', + cell: 'datetimepicker', group: gettext('Repeat'), + controlProps: { ampm: false, + placeholder: gettext('YYYY-MM-DD HH:mm:ss Z'), autoOk: true, + disablePast: true, + }, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscstart: null }; + } + } + }, { + id: 'jsscend', label: gettext('End'), type: 'datetimepicker', + cell: 'datetimepicker', group: gettext('Repeat'), + controlProps: { ampm: false, + placeholder: gettext('YYYY-MM-DD HH:mm:ss Z'), autoOk: true, + disablePast: true, + }, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscend: null }; + } + } + }, { + id: 'jsscfreq', label: gettext('Frequency'), type: 'select', + group: gettext('Repeat'), + controlProps: { allowClear: true, + placeholder: gettext('Select the frequency...'), + }, + options: [ + {'label': 'YEARLY', 'value': 'YEARLY'}, + {'label': 'MONTHLY', 'value': 'MONTHLY'}, + {'label': 'WEEKLY', 'value': 'WEEKLY'}, + {'label': 'DAILY', 'value': 'DAILY'}, + {'label': 'HOURLY', 'value': 'HOURLY'}, + {'label': 'MINUTELY', 'value': 'MINUTELY'}, + ], + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscfreq: null }; + } + } + }, { + id: 'jsscdate', label: gettext('Date'), type: 'datetimepicker', + cell: 'datetimepicker', group: gettext('Repeat'), + controlProps: { ampm: false, + placeholder: gettext('YYYYMMDD'), autoOk: true, + disablePast: true, pickerType: 'Date', format: 'yyyyMMdd', + }, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscdate: null }; + } + } + }, { + id: 'jsscmonths', label: gettext('Months'), type: 'select', + group: gettext('Repeat'), + controlProps: { allowClear: true, multiple: true, allowSelectAll: true, + placeholder: gettext('Select the months...'), + }, + options: MONTHS, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscmonths: null }; + } + } + }, { + id: 'jsscweekdays', label: gettext('Week Days'), type: 'select', + group: gettext('Repeat'), + controlProps: { allowClear: true, multiple: true, allowSelectAll: true, + placeholder: gettext('Select the weekdays...'), + }, + options: WEEKDAYS, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscweekdays: null }; + } + } + }, { + id: 'jsscmonthdays', label: gettext('Month Days'), type: 'select', + group: gettext('Repeat'), + controlProps: { allowClear: true, multiple: true, allowSelectAll: true, + placeholder: gettext('Select the month days...'), + }, + options: MONTHDAYS, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscmonthdays: null }; + } + } + }, { + id: 'jsschours', label: gettext('Hours'), type: 'select', + group: gettext('Repeat'), + controlProps: { allowClear: true, multiple: true, allowSelectAll: true, + placeholder: gettext('Select the hours...'), + }, + options: HOURS, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsschours: null }; + } + } + }, { + id: 'jsscminutes', label: gettext('Minutes'), type: 'select', + group: gettext('Repeat'), + controlProps: { allowClear: true, multiple: true, allowSelectAll: true, + placeholder: gettext('Select the minutes...'), + }, + options: MINUTES, + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsscminutes: null }; + } + } + } + ]; +} + +export function getActionSchema(obj, parentName) { + return [ + { + id: 'jsprtype', label: gettext('Type'), type: 'select', + controlProps: { allowClear: false}, group: gettext('Action'), + options: [ + {'label': 'PLSQL BLOCK', 'value': 'PLSQL_BLOCK'}, + {'label': 'STORED PROCEDURE', 'value': 'STORED_PROCEDURE'}, + ], + readonly: function(state) { return isReadOnly(obj, state, parentName); }, + deps:['jsjobtype'], + depChange: (state) => { + if (state.jsjobtype == 'p') { + return { jsprtype: null }; + } + } + }, { + id: 'jsprcode', label: gettext('Code'), type: 'sql', + group: gettext('Code'), isFullTab: true, + readonly: function(state) { + return isReadOnly(obj, state, parentName) || state.jsprtype == 'STORED_PROCEDURE'; + }, + deps:['jsprtype', 'jsjobtype'], + depChange: (state) => { + if (obj.isNew(state) && (state.jsprtype == 'STORED_PROCEDURE' || state.jsjobtype == 'p')) { + return { jsprcode: '' }; + } + } + }, { + id: 'jsprproc', label: gettext('Procedure'), type: 'select', + controlProps: { allowClear: false}, group: gettext('Action'), + options: obj.fieldOptions.procedures, + optionsLoaded: (options) => { obj.jsprocData = options; }, + readonly: function(state) { + return isReadOnly(obj, state, parentName) || state.jsprtype == 'PLSQL_BLOCK'; + }, + deps:['jsprtype', 'jsjobtype'], + depChange: (state) => { + if (obj.isNew(state) && (state.jsprtype == 'PLSQL_BLOCK' || state.jsjobtype == 'p')) { + return { jsprproc: null, jsprnoofargs : 0, jsprarguments: [] }; + } + + for(const option of obj.jsprocData) { + if (option.label == state.jsprproc) { + return { jsprnoofargs : option.no_of_args, + jsprarguments: option.arguments}; + } + } + } + }, { + id: 'jsprnoofargs', label: gettext('Number of Arguments'), + type: 'int', group: gettext('Action'), deps:['jsprtype'], + readonly: true, + }, { + id: 'jsprarguments', label: gettext('Arguments'), cell: 'string', + group: gettext('Arguments'), type: 'collection', + canAdd: false, canDelete: false, canDeleteRow: false, canEdit: false, + mode: ['create', 'edit'], + columns: parentName == 'job' ? ['argname', 'argtype', 'argdefval', 'argval'] : ['argname', 'argtype', 'argdefval'], + schema : new ProgramArgumentSchema(parentName), + } + ]; +} + +export class ProgramArgumentSchema extends BaseUISchema { + constructor(parentName) { + super(); + this.parentName = parentName; + } + + get baseFields() { + return[{ + id: 'argname', label: gettext('Name'), type: 'text', + cell: '', readonly: true, + }, { + id: 'argtype', label: gettext('Data type'), type: 'text', + cell: '', readonly: true, + }, { + id: 'argdefval', label: gettext('Default'), type: 'text', + cell: '', readonly: true, + }, { + id: 'argval', label: gettext('Value'), type: 'text', + cell: 'text', + }]; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_jobscheduler.ui.js b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_jobscheduler.ui.js new file mode 100644 index 000000000..136918640 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_jobscheduler.ui.js @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; + +export default class DBMSJobSchedulerSchema extends BaseUISchema { + constructor() { + super({ + jobid: null, + jobname: '', + jobstatus: '', + joberror: '' + }); + } + + get idAttribute() { + return 'jobid'; + } + + get baseFields() { + return [ + { + id: 'jobid', label: gettext('ID'), cell: 'int', mode: ['properties'] + }, { + id: 'jobname', label: gettext('Name'), cell: 'text' + }, { + id: 'jobstatus', label: gettext('Status'), cell: 'text' + }, { + id: 'joberror', label: gettext('Error'), cell: 'text' + }, { + id: 'jobstarttime', label: gettext('Start Time'), cell: 'datetimepicker' + }, { + id: 'jobendtime', label: gettext('End Time'), cell: 'datetimepicker' + }, { + id: 'jobnextrun', label: gettext('Next Run'), cell: 'datetimepicker' + }]; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/templates/dbms_job_scheduler/ppas/16_plus/get_job_run_details.sql b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/templates/dbms_job_scheduler/ppas/16_plus/get_job_run_details.sql new file mode 100644 index 000000000..ba55dfe24 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/templates/dbms_job_scheduler/ppas/16_plus/get_job_run_details.sql @@ -0,0 +1,13 @@ +SELECT + scjobs.dsj_job_id as jobid, scjobs.dsj_job_name as jobname, + jrd.workerpid, jrd.error as joberror, job.jobnextrun as jobnextrun, + jrd.starttime as jobstarttime, jrd.endtime as jobendtime, + CASE + WHEN jrd.status = 's' THEN 'Success' + WHEN jrd.status = 'f' THEN 'Failed' + WHEN jrd.status = 'r' THEN 'Running' + END as jobstatus +FROM sys.scheduler_0400_job scjobs + JOIN sys.job_run_details jrd ON scjobs.dsj_job_id = jrd.jobid + LEFT JOIN sys.jobs job ON scjobs.dsj_job_id = job.jobid +ORDER BY jobid; diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/utils.py new file mode 100644 index 000000000..43915bb57 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/tests/utils.py @@ -0,0 +1,485 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import sys +import json +import traceback +from urllib.parse import urlencode +from regression.python_test_utils import test_utils as utils +from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID +from pgadmin.browser.server_groups.servers.databases.tests import \ + utils as database_utils + +CONTENT_TYPE = 'html/json' +FORMAT_STRING = '{0}{1}/{2}/{3}/{4}/{5}' +TEST_DATABASE = "test_dbms_job_scheduler" +SKIP_MSG = ("The DBMS Job Scheduler is supported exclusively on EPAS servers " + "version 16 or higher.") +SKIP_MSG_EXTENSION = ("The DBMS Job Scheduler requires the presence of " + "'edb_job_scheduler' and 'dbms_scheduler' extensions in " + "the test database and that database must be listed in " + "the 'edb_job_scheduler.database_list' GUC parameter") + + +# api methods +def api_create(self): + return self.tester.post('{0}{1}/{2}/{3}/{4}/'. + format(self.url, utils.SERVER_GROUP, + self.server_id, self.db_id, + DBMS_JOB_SCHEDULER_ID), + data=json.dumps(self.data), + content_type=CONTENT_TYPE) + + +def api_delete(self, delete_id=None): + if delete_id is None and hasattr(self, 'schedule_id'): + delete_id = self.schedule_id + elif delete_id is None and hasattr(self, 'program_id'): + delete_id = self.program_id + elif delete_id is None and hasattr(self, 'job_id'): + delete_id = self.job_id + return self.tester.delete(FORMAT_STRING. + format(self.url, utils.SERVER_GROUP, + self.server_id, self.db_id, + DBMS_JOB_SCHEDULER_ID, delete_id), + data=json.dumps(self.data), + content_type=CONTENT_TYPE) + + +def api_put(self, update_id): + return self.tester.put(FORMAT_STRING. + format(self.url, utils.SERVER_GROUP, + self.server_id, self.db_id, + DBMS_JOB_SCHEDULER_ID, update_id), + data=json.dumps(self.data), + content_type=CONTENT_TYPE) + + +def api_get(self, get_id=None): + if get_id is None and hasattr(self, 'schedule_id'): + get_id = self.schedule_id + elif get_id is None and hasattr(self, 'program_id'): + get_id = self.program_id + elif get_id is None and hasattr(self, 'job_id'): + get_id = self.job_id + + return self.tester.get(FORMAT_STRING. + format(self.url, utils.SERVER_GROUP, + self.server_id, self.db_id, + DBMS_JOB_SCHEDULER_ID, get_id), + content_type=CONTENT_TYPE) + + +def api_get_msql(self, url_encode_data): + return self.tester.get("{0}{1}/{2}/{3}/{4}/?{5}". + format(self.url, utils.SERVER_GROUP, + self.server_id, self.db_id, + DBMS_JOB_SCHEDULER_ID, + urlencode(url_encode_data)), + data=json.dumps(self.data), + follow_redirects=True) + + +def create_test_database(self): + """ + This function is used to create a separate database to test DBMS Scheduler + """ + # Drop database if already exists + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, TEST_DATABASE) + + # Create a new database to test DBMS Scheduler + did = utils.create_database(self.server, TEST_DATABASE) + return TEST_DATABASE, did + + +def is_supported_version(self): + """ + This function is used to check whether version is supported to run tests. + """ + return (self.server_information['type'] == 'ppas' and + self.server_information['server_version'] >= 160000) + + +def is_dbms_job_scheduler_present(self): + """ + This function is used to check the DBMS Job Scheduler is installed. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + + query = """SELECT COUNT(*) FROM pg_extension WHERE extname IN + ('edb_job_scheduler', 'dbms_scheduler')""" + pg_cursor.execute(query) + res = pg_cursor.fetchone() + + if res and len(res) > 0 and int(res[0]) == 2: + # Get the list of databases specified for the edb_job_scheduler + pg_cursor.execute("""SHOW edb_job_scheduler.database_list""") + res = pg_cursor.fetchone() + # If database is available in the specified list than return + # True. + if res and len(res) > 0 and self.db_name in res[0]: + return True + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + return False + + +def create_job_scheduler_extensions(self): + """ + This function is used to create the extension for DBMS Job Scheduler. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + + # Create edb_job_scheduler extension if not exist. + pg_cursor.execute('''CREATE EXTENSION IF NOT EXISTS + "edb_job_scheduler"''') + + # Create dbms_scheduler extension if not exist. + pg_cursor.execute('''CREATE EXTENSION IF NOT EXISTS + "dbms_scheduler"''') + + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + return False + + +def delete_job_scheduler_extensions(self): + """ + This function is used to create the extension for DBMS Job Scheduler. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + + # Drop edb_job_scheduler extension if exist. + pg_cursor.execute('''DROP EXTENSION IF EXISTS + "edb_job_scheduler" CASCADE''') + + # Drop dbms_scheduler extension if exist. + pg_cursor.execute('''DROP EXTENSION IF EXISTS + "dbms_scheduler" CASCADE''') + + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + +def verify_dbms_schedule(self, sch_name): + """ + This function is used to verify the DBMS schedule is created or not + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + sql = """SELECT + COUNT(*) FROM sys.scheduler_0300_schedule + WHERE dss_schedule_name = '{0}'""".format(sch_name) + + pg_cursor.execute(sql) + res = pg_cursor.fetchone() + connection.close() + + if res and len(res) > 0 and int(res[0]) >= 1: + return True + except Exception: + traceback.print_exc(file=sys.stderr) + + return False + + +def create_dbms_schedule(self, sch_name): + """ + This function is used to create the dbms schedule. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + sql = """EXEC dbms_scheduler.CREATE_SCHEDULE( + schedule_name => '{0}', + start_date => '01-JUN-13 09:00:00.000000', + repeat_interval => 'FREQ=DAILY;BYDAY=MON,TUE,WED,THU,FRI;', + comments => 'This schedule executes weeknight at 5'); + """.format(sch_name) + pg_cursor.execute(sql) + connection.commit() + + pg_cursor.execute("""SELECT + dss_schedule_id FROM sys.scheduler_0300_schedule + WHERE dss_schedule_name = '{0}'""".format(sch_name)) + res = pg_cursor.fetchone() + connection.close() + + if res and len(res) > 0 and int(res[0]) >= 1: + return int(res[0]) + except Exception: + traceback.print_exc(file=sys.stderr) + + return 0 + + +def delete_dbms_schedule(self, sch_name): + """ + This function is used to delete the dbms schedule. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + sql = """EXEC dbms_scheduler.DROP_SCHEDULE('{0}')""".format( + sch_name) + pg_cursor.execute(sql) + + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + +def verify_dbms_program(self, prg_name): + """ + This function is used to verify the DBMS program is created or not + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + sql = """SELECT + COUNT(*) FROM sys.scheduler_0200_program + WHERE dsp_program_name = '{0}'""".format(prg_name) + + pg_cursor.execute(sql) + res = pg_cursor.fetchone() + connection.close() + + if res and len(res) > 0 and int(res[0]) >= 1: + return True + except Exception: + traceback.print_exc(file=sys.stderr) + + return False + + +def create_dbms_program(self, prg_name, enabled=True, + with_proc=False, proc_name=None, define_args=False): + """ + This function is used to create the dbms program. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + if with_proc: + sql = """EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => '{0}', + program_type => 'STORED_PROCEDURE', + program_action => '{1}', + enabled => {2}, + comments => 'This is a program with procedure'); + """.format(prg_name, proc_name, enabled) + + if define_args: + sql += """ EXEC dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( + program_name => '{0}', + argument_position => 0, + argument_name => 'salary', + argument_type => 'bigint', + default_value => '10000'); + """.format(prg_name) + else: + sql = """EXEC dbms_scheduler.CREATE_PROGRAM( + program_name => '{0}', + program_type => 'PLSQL_BLOCK', + program_action => 'BEGIN SELECT 1; END;', + enabled => {1}, + comments => 'This is a test program with plsql'); + """.format(prg_name, enabled) + pg_cursor.execute(sql) + connection.commit() + + pg_cursor.execute("""SELECT + dsp_program_id FROM sys.scheduler_0200_program + WHERE dsp_program_name = '{0}'""".format(prg_name)) + res = pg_cursor.fetchone() + connection.close() + + if res and len(res) > 0 and int(res[0]) >= 1: + return int(res[0]) + except Exception: + traceback.print_exc(file=sys.stderr) + + return 0 + + +def delete_dbms_program(self, prg_name): + """ + This function is used to delete the dbms program. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + sql = """EXEC dbms_scheduler.DROP_PROGRAM('{0}')""".format( + prg_name) + pg_cursor.execute(sql) + + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + +def verify_dbms_job(self, job_name): + """ + This function is used to verify the DBMS job is created or not + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + sql = """SELECT + COUNT(*) FROM sys.scheduler_0400_job + WHERE dsj_job_name = '{0}'""".format(job_name) + + pg_cursor.execute(sql) + res = pg_cursor.fetchone() + connection.close() + + if res and len(res) > 0 and int(res[0]) >= 1: + return True + except Exception: + traceback.print_exc(file=sys.stderr) + + return False + + +def create_dbms_job(self, job_name, with_proc=False, prg_name=None, + sch_name=None): + """ + This function is used to create the dbms program. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + if with_proc: + sql = """EXEC dbms_scheduler.CREATE_JOB( + job_name => '{0}', + program_name => '{1}', + schedule_name => '{2}'); + """.format(job_name, prg_name, sch_name) + else: + sql = """EXEC dbms_scheduler.CREATE_JOB( + job_name => '{0}', + job_type => 'PLSQL_BLOCK', + job_action => 'BEGIN PERFORM 1; END;', + repeat_interval => 'FREQ=YEARLY;', + start_date => '2024-02-27 00:00:00 +05:30'); + """.format(job_name) + pg_cursor.execute(sql) + connection.commit() + + pg_cursor.execute("""SELECT + dsj_job_id FROM sys.scheduler_0400_job + WHERE dsj_job_name = '{0}'""".format(job_name)) + res = pg_cursor.fetchone() + connection.close() + + if res and len(res) > 0 and int(res[0]) >= 1: + return int(res[0]) + except Exception: + traceback.print_exc(file=sys.stderr) + + return 0 + + +def delete_dbms_job(self, job_name): + """ + This function is used to delete the dbms job. + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + pg_cursor = connection.cursor() + sql = """EXEC dbms_scheduler.DROP_JOB('{0}')""".format( + job_name) + pg_cursor.execute(sql) + + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + +def clean_up(self): + # Delete extension required for job scheduler + delete_job_scheduler_extensions(self) + database_utils.disconnect_database(self, self.server_id, self.db_id) + + # Drop database if already exists + connection = utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + utils.drop_database(connection, self.db_name) diff --git a/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/utils.py b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/utils.py new file mode 100644 index 000000000..a9babfbe7 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/utils.py @@ -0,0 +1,128 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""dbms job schedular utilities""" +from pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \ + import format_arguments_from_db + +MONTHS_MAPPING = dict( + JAN='1', FEB='2', MAR='3', APR='4', MAY='5', JUN='6', + JUL='7', AUG='8', SEP='9', OCT='10', NOV='11', DEC='12' +) +WEEKDAY_MAPPING = dict( + MON='1', TUE='2', WED='3', THU='4', FRI='5', SAT='6', SUN='7' +) +# Required for reverse mapping +MONTHS_MAPPING_REV = {v: k for k, v in MONTHS_MAPPING.items()} +WEEKDAY_MAPPING_REV = {v: k for k, v in WEEKDAY_MAPPING.items()} + + +def resolve_calendar_string(calendar_string): + """ + Converts calendar_string to data + Args: + calendar_string: string to be converted + """ + freq = None + by_date = None + by_month = [] + by_monthday = [] + by_weekday = [] + by_hour = [] + by_minute = [] + + if calendar_string is not None and len(calendar_string) > 0: + # First split on the basis of semicolon + cal_list = calendar_string.split(';') + for token in cal_list: + # Split on the basis of '=' operator as tokens are + # like FREQ=MONTHLY + if not token.strip(): + continue + [token_name, token_value] = token.split('=') + token_name = token_name.strip().upper() + token_value = token_value.strip() + + if token_name == 'FREQ': + freq = token_value + elif token_name == 'BYDATE': + by_date = token_value + elif token_name == 'BYMONTH': + by_month = [MONTHS_MAPPING.get(v.upper(), v) + for v in token_value.split(',')] + elif token_name == 'BYMONTHDAY': + by_monthday = token_value.split(',') + elif token_name == 'BYDAY': + by_weekday = [WEEKDAY_MAPPING.get(v.upper(), v) + for v in token_value.split(',')] + elif token_name == 'BYHOUR': + by_hour = token_value.split(',') + elif token_name == 'BYMINUTE': + by_minute = token_value.split(',') + + return freq, by_date, by_month, by_monthday, by_weekday, by_hour, by_minute + + +def create_calendar_string(frequency, date, months, monthdays, weekdays, + hours, minutes): + """ + Create calendar string based on the given value + + Args: + frequency: + date: + months: + monthdays: + weekdays: + hours: + minutes: + """ + calendar_str = '' + + if frequency is not None: + calendar_str = 'FREQ=' + frequency + ';' + if date is not None: + calendar_str += 'BYDATE=' + str(date) + ';' + if months is not None and isinstance(months, list) and len(months) > 0: + months = [MONTHS_MAPPING_REV.get(v, v) for v in months] + calendar_str += 'BYMONTH=' + ','.join(months) + ';' + if (monthdays is not None and isinstance(monthdays, list) and + len(monthdays) > 0): + calendar_str += 'BYMONTHDAY=' + ','.join(monthdays) + ';' + if (weekdays is not None and isinstance(weekdays, list) and + len(weekdays) > 0): + weekdays = [WEEKDAY_MAPPING_REV.get(v, v) for v in weekdays] + calendar_str += 'BYDAY=' + ','.join(weekdays) + ';' + if hours is not None and isinstance(hours, list) and len(hours) > 0: + calendar_str += 'BYHOUR=' + ','.join(hours) + ';' + if minutes is not None and isinstance(minutes, list) and len(minutes) > 0: + calendar_str += 'BYMINUTE=' + ','.join(minutes) + + return calendar_str + + +def get_formatted_program_args(template_path, conn, data): + """ + This function is used to formate the program arguments. + Args: + template_path: + conn: + data: + + Returns: + + """ + if 'jsprnoofargs' in data and data['jsprnoofargs'] > 0: + frmtd_params, _ = format_arguments_from_db( + template_path, conn, data) + + if 'arguments' in frmtd_params: + new_args = [item for item in frmtd_params['arguments'] + if item['argname'] is not None] + data['jsprarguments'] = new_args diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py index 4edccb89a..09263e18d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py @@ -9,15 +9,12 @@ """Implements Functions/Procedures Node.""" -import copy import re import sys -import traceback from functools import wraps import json -from flask import render_template, request, jsonify, \ - current_app +from flask import render_template, request, jsonify from flask_babel import gettext from pgadmin.browser.server_groups.servers import databases @@ -34,6 +31,8 @@ from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry from pgadmin.utils.ajax import make_json_response, internal_server_error, \ make_response as ajax_response, gone from pgadmin.utils.driver import get_driver +from pgadmin.browser.server_groups.servers.databases.schemas.functions.utils \ + import format_arguments_from_db class FunctionModule(SchemaChildModule): @@ -475,235 +474,6 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare): status=200 ) - def _get_argument_values(self, data): - """ - This function is used to get the argument values for - function/procedure. - :param data: - :return: - """ - proargtypes = [ptype for ptype in data['proargtypenames'].split(",")] \ - if data['proargtypenames'] else [] - proargmodes = data['proargmodes'] if data['proargmodes'] else \ - ['i'] * len(proargtypes) - proargnames = data['proargnames'] if data['proargnames'] else [] - proargdefaultvals = re.split( - r',(?=(?:[^\"\']*[\"\'][^\"\']*[\"\'])*[^\"\']*$)', - data['proargdefaultvals']) if data['proargdefaultvals'] else [] - proallargtypes = data['proallargtypes'] \ - if data['proallargtypes'] else [] - - return {'proargtypes': proargtypes, 'proargmodes': proargmodes, - 'proargnames': proargnames, - 'proargdefaultvals': proargdefaultvals, - 'proallargtypes': proallargtypes} - - def _params_list_for_display(self, proargmodes_fltrd, proargtypes, - proargnames, proargdefaultvals): - """ - This function is used to prepare dictionary of arguments to - display on UI. - :param proargmodes_fltrd: - :param proargtypes: - :param proargnames: - :param proargdefaultvals: - :return: - """ - # Insert null value against the parameters which do not have - # default values. - if len(proargmodes_fltrd) > len(proargdefaultvals): - dif = len(proargmodes_fltrd) - len(proargdefaultvals) - while dif > 0: - proargdefaultvals.insert(0, '') - dif -= 1 - - param = {"arguments": [ - self._map_arguments_dict( - i, proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '', - proargtypes[i] if len(proargtypes) > i else '', - proargnames[i] if len(proargnames) > i else '', - proargdefaultvals[i] if len(proargdefaultvals) > i else '' - ) - for i in range(len(proargtypes))]} - return param - - def _display_properties_argument_list(self, proargmodes_fltrd, - proargtypes, proargnames, - proargdefaultvals): - """ - This function is used to prepare list of arguments to display on UI. - :param proargmodes_fltrd: - :param proargtypes: - :param proargnames: - :param proargdefaultvals: - :return: - """ - proargs = [self._map_arguments_list( - proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '', - proargtypes[i] if len(proargtypes) > i else '', - proargnames[i] if len(proargnames) > i else '', - proargdefaultvals[i] if len(proargdefaultvals) > i else '' - ) - for i in range(len(proargtypes))] - - return proargs - - def _format_arguments_from_db(self, data): - """ - Create Argument list of the Function. - - Args: - data: Function Data - - Returns: - Function Arguments in the following format. - [ - {'proargtypes': 'integer', 'proargmodes: 'IN', - 'proargnames': 'column1', 'proargdefaultvals': 1}, {...} - ] - Where - Arguments: - # proargtypes: Argument Types (Data Type) - # proargmodes: Argument Modes [IN, OUT, INOUT, VARIADIC] - # proargnames: Argument Name - # proargdefaultvals: Default Value of the Argument - """ - arguments = self._get_argument_values(data) - proargtypes = arguments['proargtypes'] - proargmodes = arguments['proargmodes'] - proargnames = arguments['proargnames'] - proargdefaultvals = arguments['proargdefaultvals'] - proallargtypes = arguments['proallargtypes'] - - proargmodenames = { - 'i': 'IN', 'o': 'OUT', 'b': 'INOUT', 'v': 'VARIADIC', 't': 'TABLE' - } - - # We need to put default parameter at proper location in list - # Total number of default parameters - total_default_parameters = len(proargdefaultvals) - - # Total number of parameters - total_parameters = len(proargtypes) - - # Parameters which do not have default parameters - non_default_parameters = total_parameters - total_default_parameters - - # only if we have at least one parameter with default value - if total_default_parameters > 0 and non_default_parameters > 0: - for idx in range(non_default_parameters): - # Set null value for parameter non-default parameter - proargdefaultvals.insert(idx, '') - - # The proargtypes doesn't give OUT params, so we need to fetch - # those from database explicitly, below code is written for this - # purpose. - # - # proallargtypes gives all the Function's argument including OUT, - # but we have not used that column; as the data type of this - # column (i.e. oid[]) is not supported by oidvectortypes(oidvector) - # function which we have used to fetch the datatypes - # of the other parameters. - - proargmodes_fltrd = copy.deepcopy(proargmodes) - proargnames_fltrd = [] - cnt = 0 - for m in proargmodes: - if m == 'o': # Out Mode - sql = render_template("/".join([self.sql_template_path, - 'get_out_types.sql']), - out_arg_oid=proallargtypes[cnt]) - status, out_arg_type = self.conn.execute_scalar(sql) - if not status: - return internal_server_error(errormsg=out_arg_type) - - # Insert out parameter datatype - proargtypes.insert(cnt, out_arg_type) - proargdefaultvals.insert(cnt, '') - elif m == 'v': # Variadic Mode - proargdefaultvals.insert(cnt, '') - elif m == 't': # Table Mode - proargmodes_fltrd.remove(m) - proargnames_fltrd.append(proargnames[cnt]) - - cnt += 1 - - cnt = 0 - # Map param's short form to its actual name. (ex: 'i' to 'IN') - for m in proargmodes_fltrd: - proargmodes_fltrd[cnt] = proargmodenames[m] - cnt += 1 - - # Removes Argument Names from the list if that argument is removed - # from the list - for i in proargnames_fltrd: - proargnames.remove(i) - - # Prepare list of Argument list dict to be displayed in the Data Grid. - params = self._params_list_for_display(proargmodes_fltrd, proargtypes, - proargnames, proargdefaultvals) - - # Prepare string formatted Argument to be displayed in the Properties - # panel. - proargs = self._display_properties_argument_list(proargmodes_fltrd, - proargtypes, - proargnames, - proargdefaultvals) - - proargs = {"proargs": ", ".join(proargs)} - - return params, proargs - - def _map_arguments_dict(self, argid, argmode, argtype, argname, argdefval): - """ - Returns Dict of formatted Arguments. - Args: - argid: Argument Sequence Number - argmode: Argument Mode - argname: Argument Name - argtype: Argument Type - argdef: Argument Default Value - """ - # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL - # statement gives us '-' as a default value for INOUT mode. - # so, replacing it with empty string. - if argmode == 'INOUT' and argdefval.strip() == '-': - argdefval = '' - - return {"argid": argid, - "argtype": argtype.strip() if argtype is not None else '', - "argmode": argmode, - "argname": argname, - "argdefval": argdefval} - - def _map_arguments_list(self, argmode, argtype, argname, argdef): - """ - Returns List of formatted Arguments. - Args: - argmode: Argument Mode - argname: Argument Name - argtype: Argument Type - argdef: Argument Default Value - """ - # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL - # statement gives us '-' as a default value for INOUT mode. - # so, replacing it with empty string. - if argmode == 'INOUT' and argdef.strip() == '-': - argdef = '' - - arg = '' - - if argmode: - arg += argmode + " " - if argname: - arg += argname + " " - if argtype: - arg += argtype + " " - if argdef: - arg += " DEFAULT " + argdef - - return arg.strip(" ") - def _format_proacl_from_db(self, proacl): """ Returns privileges. @@ -1581,7 +1351,9 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare): resp_data = res['rows'][0] # Get formatted Arguments - frmtd_params, frmtd_proargs = self._format_arguments_from_db(resp_data) + frmtd_params, frmtd_proargs = ( + format_arguments_from_db(self.sql_template_path, self.conn, + resp_data)) resp_data.update(frmtd_params) resp_data.update(frmtd_proargs) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/utils.py new file mode 100644 index 000000000..2ba6fdfa9 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/utils.py @@ -0,0 +1,262 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import copy +import re +from flask import render_template +from pgadmin.utils.ajax import internal_server_error + + +def get_argument_values(data): + """ + This function is used to get the argument values for + function/procedure. + :param data: + :return: + """ + proargtypes = [] + if ('proargtypenames' in data and data['proargtypenames'] and + isinstance(data['proargtypenames'], str)): + proargtypes = [ptype for ptype in data['proargtypenames'].split(",")] + elif 'proargtypenames' in data and data['proargtypenames']: + proargtypes = data['proargtypenames'] + + proargmodes = ( + data)['proargmodes'] if 'proargmodes' in data and data['proargmodes'] \ + else ['i'] * len(proargtypes) + proargnames = ( + data)['proargnames'] if 'proargnames' in data and data['proargnames'] \ + else [] + + proargdefaultvals = [] + if ('proargdefaultvals' in data and data['proargdefaultvals'] and + isinstance(data['proargdefaultvals'], str)): + proargdefaultvals = re.split( + r',(?=(?:[^\"\']*[\"\'][^\"\']*[\"\'])*[^\"\']*$)', + data['proargdefaultvals']) + elif 'proargdefaultvals' in data and data['proargdefaultvals']: + proargdefaultvals = data['proargdefaultvals'] + + proallargtypes = data['proallargtypes'] \ + if 'proallargtypes' in data and data['proallargtypes'] else [] + + return {'proargtypes': proargtypes, 'proargmodes': proargmodes, + 'proargnames': proargnames, + 'proargdefaultvals': proargdefaultvals, + 'proallargtypes': proallargtypes} + + +def params_list_for_display(proargmodes_fltrd, proargtypes, + proargnames, proargdefaultvals): + """ + This function is used to prepare dictionary of arguments to + display on UI. + :param proargmodes_fltrd: + :param proargtypes: + :param proargnames: + :param proargdefaultvals: + :return: + """ + # Insert null value against the parameters which do not have + # default values. + if len(proargmodes_fltrd) > len(proargdefaultvals): + dif = len(proargmodes_fltrd) - len(proargdefaultvals) + while dif > 0: + proargdefaultvals.insert(0, '') + dif -= 1 + + param = {"arguments": [ + map_arguments_dict( + i, proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '', + proargtypes[i] if len(proargtypes) > i else '', + proargnames[i] if len(proargnames) > i else '', + proargdefaultvals[i] if len(proargdefaultvals) > i else '' + ) + for i in range(len(proargtypes))]} + return param + + +def display_properties_argument_list(proargmodes_fltrd, proargtypes, + proargnames, proargdefaultvals): + """ + This function is used to prepare list of arguments to display on UI. + :param proargmodes_fltrd: + :param proargtypes: + :param proargnames: + :param proargdefaultvals: + :return: + """ + proargs = [map_arguments_list( + proargmodes_fltrd[i] if len(proargmodes_fltrd) > i else '', + proargtypes[i] if len(proargtypes) > i else '', + proargnames[i] if len(proargnames) > i else '', + proargdefaultvals[i] if len(proargdefaultvals) > i else '' + ) + for i in range(len(proargtypes))] + + return proargs + + +def map_arguments_dict(argid, argmode, argtype, argname, argdefval): + """ + Returns Dict of formatted Arguments. + Args: + argid: Argument Sequence Number + argmode: Argument Mode + argname: Argument Name + argtype: Argument Type + argdefval: Argument Default Value + """ + # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL + # statement gives us '-' as a default value for INOUT mode. + # so, replacing it with empty string. + if argmode == 'INOUT' and argdefval.strip() == '-': + argdefval = '' + + return {"argid": argid, + "argtype": argtype.strip() if argtype is not None else '', + "argmode": argmode, + "argname": argname, + "argdefval": argdefval} + + +def map_arguments_list(argmode, argtype, argname, argdef): + """ + Returns List of formatted Arguments. + Args: + argmode: Argument Mode + argname: Argument Name + argtype: Argument Type + argdef: Argument Default Value + """ + # The pg_get_expr(proargdefaults, 'pg_catalog.pg_class'::regclass) SQL + # statement gives us '-' as a default value for INOUT mode. + # so, replacing it with empty string. + if argmode == 'INOUT' and argdef.strip() == '-': + argdef = '' + + arg = '' + + if argmode: + arg += argmode + " " + if argname: + arg += argname + " " + if argtype: + arg += argtype + " " + if argdef: + arg += " DEFAULT " + argdef + + return arg.strip(" ") + + +def format_arguments_from_db(sql_template_path, conn, data): + """ + Create Argument list of the Function. + + Args: + sql_template_path: + conn: + data: Function Data + + Returns: + Function Arguments in the following format. + [ + {'proargtypes': 'integer', 'proargmodes: 'IN', + 'proargnames': 'column1', 'proargdefaultvals': 1}, {...} + ] + Where + Arguments: + # proargtypes: Argument Types (Data Type) + # proargmodes: Argument Modes [IN, OUT, INOUT, VARIADIC] + # proargnames: Argument Name + # proargdefaultvals: Default Value of the Argument + """ + arguments = get_argument_values(data) + proargtypes = arguments['proargtypes'] + proargmodes = arguments['proargmodes'] + proargnames = arguments['proargnames'] + proargdefaultvals = arguments['proargdefaultvals'] + proallargtypes = arguments['proallargtypes'] + + proargmodenames = { + 'i': 'IN', 'o': 'OUT', 'b': 'INOUT', 'v': 'VARIADIC', 't': 'TABLE' + } + + # We need to put default parameter at proper location in list + # Total number of default parameters + total_default_parameters = len(proargdefaultvals) + + # Total number of parameters + total_parameters = len(proargtypes) + + # Parameters which do not have default parameters + non_default_parameters = total_parameters - total_default_parameters + + # only if we have at least one parameter with default value + if total_default_parameters > 0 and non_default_parameters > 0: + for idx in range(non_default_parameters): + # Set null value for parameter non-default parameter + proargdefaultvals.insert(idx, '') + + # The proargtypes doesn't give OUT params, so we need to fetch + # those from database explicitly, below code is written for this + # purpose. + # + # proallargtypes gives all the Function's argument including OUT, + # but we have not used that column; as the data type of this + # column (i.e. oid[]) is not supported by oidvectortypes(oidvector) + # function which we have used to fetch the datatypes + # of the other parameters. + + proargmodes_fltrd = copy.deepcopy(proargmodes) + proargnames_fltrd = [] + cnt = 0 + for m in proargmodes: + if m == 'o': # Out Mode + sql = render_template("/".join([sql_template_path, + 'get_out_types.sql']), + out_arg_oid=proallargtypes[cnt]) + status, out_arg_type = conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=out_arg_type) + + # Insert out parameter datatype + proargtypes.insert(cnt, out_arg_type) + proargdefaultvals.insert(cnt, '') + elif m == 'v': # Variadic Mode + proargdefaultvals.insert(cnt, '') + elif m == 't': # Table Mode + proargmodes_fltrd.remove(m) + proargnames_fltrd.append(proargnames[cnt]) + + cnt += 1 + + cnt = 0 + # Map param's short form to its actual name. (ex: 'i' to 'IN') + for m in proargmodes_fltrd: + proargmodes_fltrd[cnt] = proargmodenames[m] + cnt += 1 + + # Removes Argument Names from the list if that argument is removed + # from the list + for i in proargnames_fltrd: + proargnames.remove(i) + + # Prepare list of Argument list dict to be displayed in the Data Grid. + params = params_list_for_display(proargmodes_fltrd, proargtypes, + proargnames, proargdefaultvals) + + # Prepare string formatted Argument to be displayed in the Properties + # panel. + proargs = display_properties_argument_list(proargmodes_fltrd, proargtypes, + proargnames, proargdefaultvals) + + proargs = {"proargs": ", ".join(proargs)} + + return params, proargs diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql index 4f2e5ccc6..a9cc7a30e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/12_plus/create_column_identity_for_restart_seq.msql @@ -1,5 +1,5 @@ ALTER TABLE IF EXISTS testschema."table_3_$%{}[]()&*^!@""'`\/#" - ADD COLUMN "col_6_$%{}[]()&*^!@""'`\/#" bigint(None, None) NOT NULL GENERATED BY DEFAULT AS IDENTITY ( CYCLE INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 10 CACHE 1 ); + ADD COLUMN "col_6_$%{}[]()&*^!@""'`\/#" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( CYCLE INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 10 CACHE 1 ); COMMENT ON COLUMN testschema."table_3_$%{}[]()&*^!@""'`\/#"."col_6_$%{}[]()&*^!@""'`\/#" IS 'demo comments'; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql index 5a232ed75..a5398f97e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/pg/default/alter_column_char.msql @@ -2,7 +2,7 @@ ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#" RENAME "col_2_$%{}[]()&*^!@""'`\/#" TO "new_col_2_$%{}[]()&*^!@""'`\/#"; ALTER TABLE testschema."table_2_$%{}[]()&*^!@""'`\/#" - ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character(None) COLLATE pg_catalog."C"; + ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character COLLATE pg_catalog."C"; ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#" ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" SET STATISTICS 5; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql index 78412bcb9..b34f0e9fb 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/12_plus/alter_column_char.msql @@ -2,7 +2,7 @@ ALTER TABLE IF EXISTS testschema."table_3_$%{}[]()&*^!@""'`\/#" RENAME "col_2_$%{}[]()&*^!@""'`\/#" TO "new_col_2_$%{}[]()&*^!@""'`\/#"; ALTER TABLE testschema."table_3_$%{}[]()&*^!@""'`\/#" - ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character(None) COLLATE pg_catalog."C"; + ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character COLLATE pg_catalog."C"; ALTER TABLE IF EXISTS testschema."table_3_$%{}[]()&*^!@""'`\/#" ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" SET STATISTICS 5; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql index 5a232ed75..a5398f97e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/tests/ppas/default/alter_column_char.msql @@ -2,7 +2,7 @@ ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#" RENAME "col_2_$%{}[]()&*^!@""'`\/#" TO "new_col_2_$%{}[]()&*^!@""'`\/#"; ALTER TABLE testschema."table_2_$%{}[]()&*^!@""'`\/#" - ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character(None) COLLATE pg_catalog."C"; + ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" TYPE character COLLATE pg_catalog."C"; ALTER TABLE IF EXISTS testschema."table_2_$%{}[]()&*^!@""'`\/#" ALTER COLUMN "new_col_2_$%{}[]()&*^!@""'`\/#" SET STATISTICS 5; diff --git a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py index 6cc7a7c15..79af7e125 100644 --- a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/__init__.py @@ -113,7 +113,7 @@ class JobScheduleView(PGChildNodeView): Methods: ------- * __init__(**kwargs) - - Method is used to initialize the JobScheduleView and it's base view. + - Method is used to initialize the JobScheduleView, and it's base view. * check_precondition() - This function will behave as a decorator which will checks @@ -125,7 +125,7 @@ class JobScheduleView(PGChildNodeView): collection. * nodes() - - This function will used to create all the child node within that + - This function will use to create all the child node within that collection. Here it will create all the schedule node. * properties(gid, sid, jid, jscid) diff --git a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js index 62e58c02d..9db15cda6 100644 --- a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js +++ b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui.js @@ -11,67 +11,7 @@ import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import { isEmptyString } from 'sources/validators'; import moment from 'moment'; - -const weekdays = [ - {label: gettext('Sunday'), value: 'Sunday'}, - {label: gettext('Monday'), value: 'Monday'}, - {label: gettext('Tuesday'), value: 'Tuesday'}, - {label: gettext('Wednesday'), value: 'Wednesday'}, - {label: gettext('Thursday'), value: 'Thursday'}, - {label: gettext('Friday'), value: 'Friday'}, - {label: gettext('Saturday'), value: 'Saturday'}, - ], - monthdays = [ - {label: gettext('1st'), value: '1st'}, {label: gettext('2nd'), value: '2nd'}, - {label: gettext('3rd'), value: '3rd'}, {label: gettext('4th'), value: '4th'}, - {label: gettext('5th'), value: '5th'}, {label: gettext('6th'), value: '6th'}, - {label: gettext('7th'), value: '7th'}, {label: gettext('8th'), value: '8th'}, - {label: gettext('9th'), value: '9th'}, {label: gettext('10th'), value: '10th'}, - {label: gettext('11th'), value: '11th'}, {label: gettext('12th'), value: '12th'}, - {label: gettext('13th'), value: '13th'}, {label: gettext('14th'), value: '14th'}, - {label: gettext('15th'), value: '15th'}, {label: gettext('16th'), value: '16th'}, - {label: gettext('17th'), value: '17th'}, {label: gettext('18th'), value: '18th'}, - {label: gettext('19th'), value: '19th'}, {label: gettext('20th'), value: '20th'}, - {label: gettext('21st'), value: '21st'}, {label: gettext('22nd'), value: '22nd'}, - {label: gettext('23rd'), value: '23rd'}, {label: gettext('24th'), value: '24th'}, - {label: gettext('25th'), value: '25th'}, {label: gettext('26th'), value: '26th'}, - {label: gettext('27th'), value: '27th'}, {label: gettext('28th'), value: '28th'}, - {label: gettext('29th'), value: '29th'}, {label: gettext('30th'), value: '30th'}, - {label: gettext('31st'), value: '31st'}, {label: gettext('Last day'), value: 'Last Day'}, - ], - months = [ - {label: gettext('January'),value: 'January'}, {label: gettext('February'),value: 'February'}, - {label: gettext('March'), value: 'March'}, {label: gettext('April'), value: 'April'}, - {label: gettext('May'), value: 'May'}, {label: gettext('June'), value: 'June'}, - {label: gettext('July'), value: 'July'}, {label: gettext('August'), value: 'August'}, - {label: gettext('September'), value: 'September'}, {label: gettext('October'), value: 'October'}, - {label: gettext('November'), value: 'November'}, {label: gettext('December'), value: 'December'}, - ], - hours = [ - {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'}, - {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'}, - {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'}, - {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'}, - {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'}, - {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'}, - ], - minutes = [ - {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'}, - {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'}, - {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'}, - {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'}, - {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'}, - {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'}, - {label: gettext('24'), value: '24'}, {label: gettext('25'), value: '25'}, {label: gettext('26'), value: '26'}, {label: gettext('27'), value: '27'}, - {label: gettext('28'), value: '28'}, {label: gettext('29'), value: '29'}, {label: gettext('30'), value: '30'}, {label: gettext('31'), value: '31'}, - {label: gettext('32'), value: '32'}, {label: gettext('33'), value: '33'}, {label: gettext('34'), value: '34'}, {label: gettext('35'), value: '35'}, - {label: gettext('36'), value: '36'}, {label: gettext('37'), value: '37'}, {label: gettext('38'), value: '38'}, {label: gettext('39'), value: '39'}, - {label: gettext('40'), value: '40'}, {label: gettext('41'), value: '41'}, {label: gettext('42'), value: '42'}, {label: gettext('43'), value: '43'}, - {label: gettext('44'), value: '44'}, {label: gettext('45'), value: '45'}, {label: gettext('46'), value: '46'}, {label: gettext('47'), value: '47'}, - {label: gettext('48'), value: '48'}, {label: gettext('49'), value: '49'}, {label: gettext('50'), value: '50'}, {label: gettext('51'), value: '51'}, - {label: gettext('52'), value: '52'}, {label: gettext('53'), value: '53'}, {label: gettext('54'), value: '54'}, {label: gettext('55'), value: '55'}, - {label: gettext('56'), value: '56'}, {label: gettext('57'), value: '57'}, {label: gettext('58'), value: '58'}, {label: gettext('59'), value: '59'}, - ]; +import { WEEKDAYS, MONTHDAYS, MONTHS, HOURS, MINUTES } from '../../../../../../static/js/constants'; export class ExceptionsSchema extends BaseUISchema { constructor(fieldOptions={}, initValues={}) { @@ -178,7 +118,7 @@ export class DaysSchema extends BaseUISchema { placeholder: gettext('Select the weekdays...'), formatter: BooleanArrayFormatter, }, - options: weekdays, + options: WEEKDAYS, }, { id: 'jscmonthdays', label: gettext('Month Days'), type: 'select', group: gettext('Days'), @@ -186,7 +126,7 @@ export class DaysSchema extends BaseUISchema { placeholder: gettext('Select the month days...'), formatter: BooleanArrayFormatter, }, - options: monthdays, + options: MONTHDAYS, }, { id: 'jscmonths', label: gettext('Months'), type: 'select', group: gettext('Days'), @@ -194,7 +134,7 @@ export class DaysSchema extends BaseUISchema { placeholder: gettext('Select the months...'), formatter: BooleanArrayFormatter, }, - options: months, + options: MONTHS, } ]; } @@ -220,7 +160,7 @@ export class TimesSchema extends BaseUISchema { placeholder: gettext('Select the hours...'), formatter: BooleanArrayFormatter, }, - options: hours, + options: HOURS, }, { id: 'jscminutes', label: gettext('Minutes'), type: 'select', group: gettext('Times'), @@ -228,7 +168,7 @@ export class TimesSchema extends BaseUISchema { placeholder: gettext('Select the minutes...'), formatter: BooleanArrayFormatter, }, - options: minutes, + options: MINUTES, } ]; } @@ -244,11 +184,11 @@ export default class PgaJobScheduleSchema extends BaseUISchema { jscenabled: true, jscstart: null, jscend: null, - jscweekdays: _.map(weekdays, function() { return false; }), - jscmonthdays: _.map(monthdays, function() { return false; }), - jscmonths: _.map(months, function() { return false; }), - jschours: _.map(hours, function() { return false; }), - jscminutes: _.map(minutes, function() { return false; }), + jscweekdays: _.map(WEEKDAYS, function() { return false; }), + jscmonthdays: _.map(MONTHDAYS, function() { return false; }), + jscmonths: _.map(MONTHS, function() { return false; }), + jschours: _.map(HOURS, function() { return false; }), + jscminutes: _.map(MINUTES, function() { return false; }), jscexceptions: [], ...initValues, }); @@ -324,7 +264,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema { controlProps: { formatter: { fromRaw: (backendVal)=> { - return obj.customFromRaw(backendVal, weekdays); + return obj.customFromRaw(backendVal, WEEKDAYS); } }, } @@ -334,7 +274,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema { controlProps: { formatter: { fromRaw: (backendVal)=> { - return obj.customFromRaw(backendVal, monthdays); + return obj.customFromRaw(backendVal, MONTHDAYS); } }, } @@ -344,7 +284,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema { controlProps: { formatter: { fromRaw: (backendVal)=> { - return obj.customFromRaw(backendVal, months); + return obj.customFromRaw(backendVal, MONTHS); } }, } @@ -354,7 +294,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema { controlProps: { formatter: { fromRaw: (backendVal)=> { - return obj.customFromRaw(backendVal, hours); + return obj.customFromRaw(backendVal, HOURS); } }, } @@ -364,7 +304,7 @@ export default class PgaJobScheduleSchema extends BaseUISchema { controlProps: { formatter: { fromRaw: (backendVal)=> { - return obj.customFromRaw(backendVal, minutes); + return obj.customFromRaw(backendVal, MINUTES); } }, } diff --git a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py index 3a2e14c49..13b020864 100644 --- a/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py +++ b/web/pgadmin/browser/server_groups/servers/pgagent/schedules/tests/utils.py @@ -1,5 +1,12 @@ -import sys -import traceback +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2024, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + import os import json from urllib.parse import urlencode diff --git a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql index 54576e03b..3ebaab484 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql +++ b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_login_role.msql @@ -6,5 +6,4 @@ CREATE ROLE "Role1_$%{}[]()&*^!@""'`\/#" WITH INHERIT REPLICATION BYPASSRLS - CONNECTION LIMIT -1 - PASSWORD 'xxxxxx'; + CONNECTION LIMIT -1; diff --git a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql index 63630a79f..5d0c2026e 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql +++ b/web/pgadmin/browser/server_groups/servers/roles/tests/default/create_role.msql @@ -6,5 +6,4 @@ CREATE ROLE "Role1_$%{}[]()&*^!@""'`\/#" WITH INHERIT NOREPLICATION NOBYPASSRLS - CONNECTION LIMIT -1 - PASSWORD 'xxxxxx'; + CONNECTION LIMIT -1; diff --git a/web/pgadmin/browser/static/js/constants.js b/web/pgadmin/browser/static/js/constants.js index 5fd9bd92b..46abc3a45 100644 --- a/web/pgadmin/browser/static/js/constants.js +++ b/web/pgadmin/browser/static/js/constants.js @@ -1,3 +1,14 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; + export const AUTH_METHODS = { INTERNAL: 'internal', LDAP: 'ldap', @@ -32,3 +43,64 @@ export const BROWSER_PANELS = { USER_MANAGEMENT: 'id-user-management', IMPORT_EXPORT_SERVERS: 'id-import-export-servers' }; + +export const WEEKDAYS = [ + {label: gettext('Sunday'), value: '7'}, + {label: gettext('Monday'), value: '1'}, + {label: gettext('Tuesday'), value: '2'}, + {label: gettext('Wednesday'), value: '3'}, + {label: gettext('Thursday'), value: '4'}, + {label: gettext('Friday'), value: '5'}, + {label: gettext('Saturday'), value: '6'}, + ], + MONTHDAYS = [ + {label: gettext('1st'), value: '1'}, {label: gettext('2nd'), value: '2'}, + {label: gettext('3rd'), value: '3'}, {label: gettext('4th'), value: '4'}, + {label: gettext('5th'), value: '5'}, {label: gettext('6th'), value: '6'}, + {label: gettext('7th'), value: '7'}, {label: gettext('8th'), value: '8'}, + {label: gettext('9th'), value: '9'}, {label: gettext('10th'), value: '10'}, + {label: gettext('11th'), value: '11'}, {label: gettext('12th'), value: '12'}, + {label: gettext('13th'), value: '13'}, {label: gettext('14th'), value: '14'}, + {label: gettext('15th'), value: '15'}, {label: gettext('16th'), value: '16'}, + {label: gettext('17th'), value: '17'}, {label: gettext('18th'), value: '18'}, + {label: gettext('19th'), value: '19'}, {label: gettext('20th'), value: '20'}, + {label: gettext('21st'), value: '21'}, {label: gettext('22nd'), value: '22'}, + {label: gettext('23rd'), value: '23'}, {label: gettext('24th'), value: '24'}, + {label: gettext('25th'), value: '25'}, {label: gettext('26th'), value: '26'}, + {label: gettext('27th'), value: '27'}, {label: gettext('28th'), value: '28'}, + {label: gettext('29th'), value: '29'}, {label: gettext('30th'), value: '30'}, + {label: gettext('31st'), value: '31'}, + ], + MONTHS = [ + {label: gettext('January'),value: '1'}, {label: gettext('February'),value: '2'}, + {label: gettext('March'), value: '3'}, {label: gettext('April'), value: '4'}, + {label: gettext('May'), value: '5'}, {label: gettext('June'), value: '6'}, + {label: gettext('July'), value: '7'}, {label: gettext('August'), value: '8'}, + {label: gettext('September'), value: '9'}, {label: gettext('October'), value: '10'}, + {label: gettext('November'), value: '11'}, {label: gettext('December'), value: '12'}, + ], + HOURS = [ + {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'}, + {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'}, + {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'}, + {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'}, + {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'}, + {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'}, + ], + MINUTES = [ + {label: gettext('00'), value: '00'}, {label: gettext('01'), value: '01'}, {label: gettext('02'), value: '02'}, {label: gettext('03'), value: '03'}, + {label: gettext('04'), value: '04'}, {label: gettext('05'), value: '05'}, {label: gettext('06'), value: '06'}, {label: gettext('07'), value: '07'}, + {label: gettext('08'), value: '08'}, {label: gettext('09'), value: '09'}, {label: gettext('10'), value: '10'}, {label: gettext('11'), value: '11'}, + {label: gettext('12'), value: '12'}, {label: gettext('13'), value: '13'}, {label: gettext('14'), value: '14'}, {label: gettext('15'), value: '15'}, + {label: gettext('16'), value: '16'}, {label: gettext('17'), value: '17'}, {label: gettext('18'), value: '18'}, {label: gettext('19'), value: '19'}, + {label: gettext('20'), value: '20'}, {label: gettext('21'), value: '21'}, {label: gettext('22'), value: '22'}, {label: gettext('23'), value: '23'}, + {label: gettext('24'), value: '24'}, {label: gettext('25'), value: '25'}, {label: gettext('26'), value: '26'}, {label: gettext('27'), value: '27'}, + {label: gettext('28'), value: '28'}, {label: gettext('29'), value: '29'}, {label: gettext('30'), value: '30'}, {label: gettext('31'), value: '31'}, + {label: gettext('32'), value: '32'}, {label: gettext('33'), value: '33'}, {label: gettext('34'), value: '34'}, {label: gettext('35'), value: '35'}, + {label: gettext('36'), value: '36'}, {label: gettext('37'), value: '37'}, {label: gettext('38'), value: '38'}, {label: gettext('39'), value: '39'}, + {label: gettext('40'), value: '40'}, {label: gettext('41'), value: '41'}, {label: gettext('42'), value: '42'}, {label: gettext('43'), value: '43'}, + {label: gettext('44'), value: '44'}, {label: gettext('45'), value: '45'}, {label: gettext('46'), value: '46'}, {label: gettext('47'), value: '47'}, + {label: gettext('48'), value: '48'}, {label: gettext('49'), value: '49'}, {label: gettext('50'), value: '50'}, {label: gettext('51'), value: '51'}, + {label: gettext('52'), value: '52'}, {label: gettext('53'), value: '53'}, {label: gettext('54'), value: '54'}, {label: gettext('55'), value: '55'}, + {label: gettext('56'), value: '56'}, {label: gettext('57'), value: '57'}, {label: gettext('58'), value: '58'}, {label: gettext('59'), value: '59'}, + ]; diff --git a/web/pgadmin/help/static/js/help.js b/web/pgadmin/help/static/js/help.js index 815f99f3a..d30eb952a 100644 --- a/web/pgadmin/help/static/js/help.js +++ b/web/pgadmin/help/static/js/help.js @@ -27,10 +27,10 @@ export function getHelpUrl(base_path, file, version) { return url + file; } -export function getEPASHelpUrl(version) { +export function getEPASHelpUrl(version, epasURL=null) { let major = Math.floor(version / 10000), minor = Math.floor(version / 100) - (major * 100), - epasHelp11Plus = 'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_sql/', + epasHelp11Plus = epasURL??'https://www.enterprisedb.com/docs/epas/$VERSION$/epas_compat_sql/', epasHelp = 'https://www.enterprisedb.com/docs/epas/$VERSION$/', url = ''; diff --git a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx index a9daa8145..f8645735f 100644 --- a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx +++ b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx @@ -210,7 +210,7 @@ export default function CollectionNodeProperties({ setLoaderText(gettext('Loading...')); - if (nodeData._type.indexOf('coll-') > -1 && !_.isUndefined(nodeObj.getSchema)) { + if (!_.isUndefined(nodeObj.getSchema)) { schemaRef.current = nodeObj.getSchema?.call(nodeObj, treeNodeInfo, nodeData); schemaRef.current?.fields.forEach((field) => { if (node.columns.indexOf(field.id) > -1) { diff --git a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx index 3035ea222..0a76b229b 100644 --- a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx +++ b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx @@ -146,7 +146,7 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD let fullUrl = ''; if (server.server_type == 'ppas' && node.epasHelp) { - fullUrl = getEPASHelpUrl(server.version); + fullUrl = getEPASHelpUrl(server.version, node.epasURL); } else if (node.sqlCreateHelp == '' && node.sqlAlterHelp != '') { fullUrl = getHelpUrl(helpUrl, node.sqlAlterHelp, server.version); } else if (node.sqlCreateHelp != '' && node.sqlAlterHelp == '') { diff --git a/web/pgadmin/misc/properties/Properties.jsx b/web/pgadmin/misc/properties/Properties.jsx index 56924bd04..146761be6 100644 --- a/web/pgadmin/misc/properties/Properties.jsx +++ b/web/pgadmin/misc/properties/Properties.jsx @@ -1,3 +1,12 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + import React from 'react'; import CollectionNodeProperties from './CollectionNodeProperties'; import ErrorBoundary from '../../static/js/helpers/ErrorBoundary'; @@ -9,6 +18,7 @@ import gettext from 'sources/gettext'; import { Box, makeStyles } from '@material-ui/core'; import { usePgAdmin } from '../../static/js/BrowserComponent'; import PropTypes from 'prop-types'; +import _ from 'lodash'; const useStyles = makeStyles((theme) => ({ root: { @@ -20,15 +30,22 @@ const useStyles = makeStyles((theme) => ({ })); function Properties(props) { - const isCollection = props.nodeData?._type?.startsWith('coll-'); + const isCollection = props.nodeData?._type?.startsWith('coll-') || props.nodeData?._type == 'dbms_job_scheduler'; const classes = useStyles(); const pgAdmin = usePgAdmin(); + let noPropertyMsg = ''; - if(!props.node) { + if (!props.node) { + noPropertyMsg = gettext('Please select an object in the tree view.'); + } else if (!_.isUndefined(props.node.hasProperties) && !props.node.hasProperties) { + noPropertyMsg = gettext('No information is available for the selected object.'); + } + + if(noPropertyMsg) { return ( - + ); diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js index daa8bf945..a56ef1b68 100644 --- a/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js +++ b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js @@ -139,7 +139,7 @@ _.extend(pgBrowser.browserTreeState, { update_cache: function(item) { let data = item && pgBrowser.tree.itemData(item), treeHierarchy = pgBrowser.tree.getTreeNodeHierarchy(item), - topParent = undefined, + topParent, pathIDs = pgBrowser.tree.pathId(pgBrowser.tree.parent(item)), oldPath = pathIDs.join(), path = [], @@ -204,7 +204,7 @@ _.extend(pgBrowser.browserTreeState, { this.update_database_status(item); if (data._type == self.parent || data._type == 'database') { - if (topParent in treeData && 'paths' in treeData[topParent]) { + if (treeData?.[topParent]?.['paths'] && self.current_state?.[topParent]?.['paths']) { treeData[topParent]['paths'] = self.current_state[topParent]['paths']; self.save_state(); } @@ -213,14 +213,13 @@ _.extend(pgBrowser.browserTreeState, { if (pgBrowser.tree.isClosed(item)) { let tmpTreeData = self.current_state[topParent]['paths'], - databaseId = undefined; + databaseId; if (treeHierarchy.hasOwnProperty('database')) databaseId = treeHierarchy['database']['id']; if (!_.isUndefined(tmpTreeData) && !_.isUndefined(tmpTreeData.length)) { - let tcnt = 0, - tmpItemDataStr = undefined; + let tcnt = 0, tmpItemDataStr; _.each(tmpTreeData, function(tData) { if (_.isUndefined(tData)) return; @@ -271,7 +270,7 @@ _.extend(pgBrowser.browserTreeState, { if (!_.isUndefined(tmpTreeData) && ('paths' in tmpTreeData) && !_.isUndefined(tmpTreeData['paths'].length)) { let tmpTreeDataPaths = [...tmpTreeData['paths']], - databaseId = undefined; + databaseId; if (treeHierarchy.hasOwnProperty('database')) databaseId = treeHierarchy['database']['id']; @@ -335,7 +334,7 @@ _.extend(pgBrowser.browserTreeState, { let topParent = treeHierarchy?.[this.parent]['_id'], selectedItem = pgBrowser.tree.itemData(pgBrowser.tree.selected()), - databaseItem = undefined; + databaseItem; selectedItem = selectedItem ? selectedItem.id : undefined; diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 790bdae8c..9ea96fb03 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -137,3 +137,6 @@ class MessageType: INFO = 'Info', CLOSE = 'Close', WARNING = 'Warning' + + +DBMS_JOB_SCHEDULER_ID = 999999 diff --git a/web/regression/re_sql/tests/test_resql.py b/web/regression/re_sql/tests/test_resql.py index b9b5d77e2..534953957 100644 --- a/web/regression/re_sql/tests/test_resql.py +++ b/web/regression/re_sql/tests/test_resql.py @@ -13,7 +13,6 @@ import traceback from urllib.parse import urlencode from flask import url_for import regression -import config from regression import parent_node_dict from pgadmin.utils.route import BaseTestGenerator from regression.python_test_utils import test_utils as utils @@ -21,7 +20,7 @@ from pgadmin.browser.server_groups.servers.databases.tests import \ utils as database_utils from pgadmin.utils.versioned_template_loader import \ get_version_mapping_directories -from config import PG_DEFAULT_DRIVER +from pgadmin.utils.constants import DBMS_JOB_SCHEDULER_ID def create_resql_module_list(all_modules, exclude_pkgs, for_modules): @@ -213,6 +212,8 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator): # fsid represents Foreign Server oid elif arg == 'fsid' and 'fsid' in self.parent_ids: options['fsid'] = int(self.parent_ids['fsid']) + elif arg == 'jsid': + options['jsid'] = DBMS_JOB_SCHEDULER_ID else: if object_id is not None: try: @@ -447,7 +448,8 @@ class ReverseEngineeredSQLTestCases(BaseTestGenerator): # urlencode msql_data = { key: json.dumps(val) - if isinstance(val, dict) or isinstance(val, list) else val + if isinstance(val, dict) or isinstance(val, list) else + (val if val is not None else 'null') for key, val in scenario['data'].items()} params = urlencode(msql_data) diff --git a/web/webpack.config.js b/web/webpack.config.js index b02958fa2..265732808 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -502,6 +502,7 @@ module.exports = [{ 'pure|pgadmin.node.compound_trigger', 'pure|pgadmin.node.aggregate', 'pure|pgadmin.node.operator', + 'pure|pgadmin.node.dbms_job_scheduler', ], }, }, diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 07da0929c..b3035f4ef 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -86,6 +86,10 @@ let webpackShimConfig = { 'pgadmin.node.compound_trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/compound_triggers/static/js/compound_trigger'), 'pgadmin.node.constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints'), 'pgadmin.node.database': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/static/js/database'), + 'pgadmin.node.dbms_job_scheduler': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/static/js/dbms_job_scheduler'), + 'pgadmin.node.dbms_job': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_jobs/static/js/dbms_job'), + 'pgadmin.node.dbms_program': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_programs/static/js/dbms_program'), + 'pgadmin.node.dbms_schedule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/dbms_job_scheduler/dbms_schedules/static/js/dbms_schedule'), 'pgadmin.node.domain': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain'), 'pgadmin.node.domain_constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints'), 'pgadmin.node.event_trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/event_triggers/static/js/event_trigger'),