diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 933e6fc20..7149b83cc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,10 @@ Unreleased ---------- * nothing unreleased +[4.20.12] +--------- +* feat: added support for testing sftp connection inside ``EnterpriseCustomerReportingConfiguration`` instance. + [4.20.11] --------- * fix: setting existing user group membership statuses diff --git a/enterprise/__init__.py b/enterprise/__init__.py index 71625b1ce..2ce26c585 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.20.11" +__version__ = "4.20.12" diff --git a/enterprise/admin/__init__.py b/enterprise/admin/__init__.py index 743dce963..e4849bbaa 100644 --- a/enterprise/admin/__init__.py +++ b/enterprise/admin/__init__.py @@ -4,13 +4,14 @@ import logging +import paramiko from config_models.admin import ConfigurationModelAdmin from django_object_actions import DjangoObjectActions from edx_rbac.admin import UserRoleAssignmentAdmin from simple_history.admin import SimpleHistoryAdmin from django.conf import settings -from django.contrib import admin, auth +from django.contrib import admin, auth, messages from django.core.paginator import Paginator from django.db import connection from django.db.models import Q @@ -975,6 +976,56 @@ def get_fields(self, request, obj=None): return fields + def get_readonly_fields(self, request, obj=None): + """ + Conditionally add the test_sftp_server to the readonly fields. + """ + readonly_fields = list(super().get_readonly_fields(request, obj)) + if obj and obj.delivery_method == models.EnterpriseCustomerReportingConfiguration.DELIVERY_METHOD_SFTP: + readonly_fields.append('test_sftp_server') + return readonly_fields + + def test_sftp_server(self, obj): + """ + Add a button to test the SFTP server connection. + """ + return format_html( + 'Test SFTP Server Connection', + reverse('admin:test_sftp_connection', args=[obj.pk]), + ) + + test_sftp_server.short_description = 'Test SFTP Server' + test_sftp_server.allow_tags = True + + def get_urls(self): + """ + Extend the admin URLs to include the custom test server URL. + """ + urls = super().get_urls() + custom_urls = [ + path('test-sftp-connection//', self.admin_site.admin_view(self.test_sftp_connection), + name='test_sftp_connection'), + ] + return custom_urls + urls + + def test_sftp_connection(self, request, pk): + """ + Custom admin view to test the SFTP server connection. + """ + config = self.get_object(request, pk) + if config: + try: + transport = paramiko.Transport((config.sftp_hostname, config.sftp_port)) + transport.connect(username=config.sftp_username, password=config.decrypted_sftp_password) + sftp = paramiko.SFTPClient.from_transport(transport) + sftp.close() + transport.close() + self.message_user(request, "Successfully connected to the SFTP server.") + except Exception as e: # pylint: disable=broad-except + self.message_user(request, f"Failed to connect to the SFTP server: {e}", level=messages.ERROR) + + return HttpResponseRedirect(reverse('admin:enterprise_enterprisecustomerreportingconfiguration_changelist')) + class BigTableMysqlPaginator(Paginator): """ diff --git a/requirements/base.in b/requirements/base.in index b54f17646..d0b454396 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -31,6 +31,7 @@ edx-tincan-py35 edx-toggles jsondiff jsonfield +paramiko path.py pillow python-dateutil diff --git a/requirements/dev.txt b/requirements/dev.txt index 349e71358..4d227e0f6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -80,6 +80,12 @@ backports-zoneinfo[tzdata]==0.2.1 ; python_version < "3.9" # celery # django # kombu +bcrypt==4.1.3 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt + # paramiko beautifulsoup4==4.12.3 # via # -r requirements/doc.txt @@ -181,6 +187,7 @@ cryptography==42.0.8 # -r requirements/test.txt # django-fernet-fields-v2 # jwcrypto + # paramiko # pgpy # pyjwt # pyopenssl @@ -193,7 +200,7 @@ defusedxml==0.7.1 # -r requirements/test-master.txt # -r requirements/test.txt # djangorestframework-xml -diff-cover==9.0.0 +diff-cover==9.1.0 # via -r requirements/test.txt dill==0.3.8 # via pylint @@ -393,7 +400,7 @@ factory-boy==3.3.0 # -c requirements/constraints.txt # -r requirements/doc.txt # -r requirements/test.txt -faker==25.9.1 +faker==26.0.0 # via # -r requirements/doc.txt # -r requirements/test.txt @@ -480,7 +487,7 @@ kombu==5.3.7 # -r requirements/test-master.txt # -r requirements/test.txt # celery -lxml[html-clean,html_clean]==5.2.2 +lxml[html_clean]==5.2.2 # via # edx-i18n-tools # lxml-html-clean @@ -538,6 +545,11 @@ packaging==24.1 # snowflake-connector-python # sphinx # tox +paramiko==3.4.0 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt path==16.11.0 # via # -r requirements/doc.txt @@ -644,7 +656,7 @@ pyjwt[crypto]==2.8.0 # edx-drf-extensions # edx-rest-api-client # snowflake-connector-python -pylint==3.2.3 +pylint==3.2.4 # via # edx-lint # pylint-celery @@ -670,6 +682,7 @@ pynacl==1.5.0 # -r requirements/test-master.txt # -r requirements/test.txt # edx-django-utils + # paramiko pyopenssl==24.1.0 # via # -r requirements/doc.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 7698b633c..890f55f99 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -53,6 +53,10 @@ backports-zoneinfo[tzdata]==0.2.1 ; python_version < "3.9" # celery # django # kombu +bcrypt==4.1.3 + # via + # -r requirements/test-master.txt + # paramiko beautifulsoup4==4.12.3 # via pydata-sphinx-theme billiard==4.2.0 @@ -111,6 +115,7 @@ cryptography==42.0.8 # -r requirements/test-master.txt # django-fernet-fields-v2 # jwcrypto + # paramiko # pgpy # pyjwt # pyopenssl @@ -237,7 +242,7 @@ factory-boy==3.3.0 # via # -c requirements/constraints.txt # -r requirements/doc.in -faker==25.9.1 +faker==26.0.0 # via factory-boy filelock==3.14.0 # via @@ -312,6 +317,8 @@ packaging==24.1 # pytest # snowflake-connector-python # sphinx +paramiko==3.4.0 + # via -r requirements/test-master.txt path==16.11.0 # via # -r requirements/test-master.txt @@ -374,6 +381,7 @@ pynacl==1.5.0 # via # -r requirements/test-master.txt # edx-django-utils + # paramiko pyopenssl==24.1.0 # via # -r requirements/test-master.txt diff --git a/requirements/edx-platform-constraints.txt b/requirements/edx-platform-constraints.txt index 2d90ac899..5cc952dde 100644 --- a/requirements/edx-platform-constraints.txt +++ b/requirements/edx-platform-constraints.txt @@ -434,7 +434,7 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.20.8 +edx-enterprise==4.20.11 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in @@ -730,7 +730,7 @@ openedx-events==9.10.0 # edx-event-bus-redis # event-tracking # ora2 -openedx-filters==1.8.1 +openedx-filters==1.9.0 # via # -r requirements/edx/kernel.in # lti-consumer-xblock diff --git a/requirements/test-master.txt b/requirements/test-master.txt index 22b538c82..781479937 100644 --- a/requirements/test-master.txt +++ b/requirements/test-master.txt @@ -39,6 +39,8 @@ backports-zoneinfo[tzdata]==0.2.1 ; python_version < "3.9" # celery # django # kombu +bcrypt==4.1.3 + # via paramiko billiard==4.2.0 # via celery bleach==6.1.0 @@ -92,6 +94,7 @@ cryptography==42.0.8 # -r requirements/base.in # django-fernet-fields-v2 # jwcrypto + # paramiko # pgpy # pyjwt # pyopenssl @@ -307,6 +310,8 @@ packaging==24.1 # -c requirements/edx-platform-constraints.txt # drf-yasg # snowflake-connector-python +paramiko==3.4.0 + # via -r requirements/base.in path==16.11.0 # via # -c requirements/edx-platform-constraints.txt @@ -360,6 +365,7 @@ pynacl==1.5.0 # via # -c requirements/edx-platform-constraints.txt # edx-django-utils + # paramiko pyopenssl==24.1.0 # via # -c requirements/edx-platform-constraints.txt diff --git a/requirements/test.txt b/requirements/test.txt index 71e9078b6..0ee58874d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -45,6 +45,10 @@ backports-zoneinfo[tzdata]==0.2.1 ; python_version < "3.9" # celery # django # kombu +bcrypt==4.1.3 + # via + # -r requirements/test-master.txt + # paramiko # via # -r requirements/test-master.txt # celery @@ -100,6 +104,7 @@ cryptography==42.0.8 # -r requirements/test-master.txt # django-fernet-fields-v2 # jwcrypto + # paramiko # pgpy # pyjwt # pyopenssl @@ -110,7 +115,7 @@ defusedxml==0.7.1 # via # -r requirements/test-master.txt # djangorestframework-xml -diff-cover==9.0.0 +diff-cover==9.1.0 # via -r requirements/test.in # via # -c requirements/common_constraints.txt @@ -220,7 +225,7 @@ factory-boy==3.3.0 # via # -c requirements/constraints.txt # -r requirements/test.in -faker==25.9.1 +faker==26.0.0 # via factory-boy filelock==3.14.0 # via @@ -292,6 +297,8 @@ packaging==24.1 # drf-yasg # pytest # snowflake-connector-python +paramiko==3.4.0 + # via -r requirements/test-master.txt path==16.11.0 # via # -r requirements/test-master.txt @@ -348,6 +355,7 @@ pynacl==1.5.0 # via # -r requirements/test-master.txt # edx-django-utils + # paramiko pyopenssl==24.1.0 # via # -r requirements/test-master.txt