Skip to content

Commit d946469

Browse files
authored
Merge branch 'master' into dill21yu-patch-1
2 parents 1a7b4bb + 666cf4e commit d946469

File tree

54 files changed

+3960
-893
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3960
-893
lines changed

.github/workflows/pr-comments.yml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
name: PR Comments
22

33
on:
4-
pull_request_target:
4+
pull_request:
55
branches:
66
- master
77

8-
permissions:
9-
pull-requests: write
10-
118
jobs:
129
backend-tests-and-coverage:
1310
runs-on: ubuntu-latest
@@ -56,10 +53,8 @@ jobs:
5653
uses: MishaKav/pytest-coverage-comment@v1
5754
with:
5855
pytest-xml-coverage-path: ./reports/code-cov/coverage.xml
59-
junitxml-path: ./reports/pytest/test_report.xml
60-
junitxml-title: Pytest Report
61-
title: Python Coverage Report
62-
badge-title: Python Code Coverage
56+
title: Backend Code Coverage Report
57+
badge-title: Coverage
6358
report-only-changed-files: true
6459
xml-skip-covered: true
6560
remove-link-from-badge: true
@@ -121,7 +116,7 @@ jobs:
121116
uses: MishaKav/jest-coverage-comment@v1
122117
with:
123118
coverage-summary-path: ./reports/jest/coverage-summary.json
124-
summary-title: UI Coverage Report
119+
summary-title: UI Code Coverage Report
125120
badge-title: Coverage
126121
hide-comment: false
127122
create-new-comment: false

.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
# pip requirements file
1010
.requirements
1111

12+
# Other generated build artifacts by generate_requirements.py
13+
desktop/core/requirements-arm64-*.txt
14+
desktop/core/requirements-aarch64-*.txt
15+
desktop/core/requirements-x86_64-*.txt
16+
17+
# Python version file
18+
.python-version
19+
1220
# Python compiled files
1321
*.pyc
1422
*.pyo
@@ -32,6 +40,13 @@ metastore_db
3240
VERSION_INFO
3341
npm_dist
3442

43+
# Build marker files generated by Makefile
44+
.npm-install-lock
45+
desktop/core/ext-env-pip-install
46+
47+
# Python version-specific directories
48+
desktop/core/3.*/
49+
3550
# Generated configuration
3651
desktop/conf/*.ini
3752

apps/about/src/about/api.py

Lines changed: 43 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -16,75 +16,59 @@
1616
# limitations under the License.
1717

1818
import logging
19+
from typing import Any
1920

2021
from rest_framework import status
22+
from rest_framework.request import Request
2123
from rest_framework.response import Response
24+
from rest_framework.views import APIView
2225

23-
from desktop.auth.backend import is_admin
24-
from desktop.lib.conf import coerce_bool
26+
from about.serializer import UsageAnalyticsSerializer
27+
from desktop.auth.api_permissions import IsAdminUser
2528
from desktop.models import Settings
2629

2730
LOG = logging.getLogger()
2831

2932

30-
def get_usage_analytics(request) -> Response:
33+
class UsageAnalyticsAPI(APIView):
3134
"""
32-
Retrieve the user preference for analytics settings.
35+
Provides GET and PUT handlers for the usage analytics setting.
3336
34-
Args:
35-
request (Request): The HTTP request object.
36-
37-
Returns:
38-
Response: JSON response containing the analytics_enabled preference or an error message.
39-
40-
Raises:
41-
403: If the user is not a Hue admin.
42-
500: If there is an error retrieving preference.
43-
"""
44-
if not is_admin(request.user):
45-
return Response({'message': "You must be a Hue admin to access this endpoint."}, status=status.HTTP_403_FORBIDDEN)
46-
47-
try:
48-
settings = Settings.get_settings()
49-
return Response({'analytics_enabled': settings.collect_usage}, status=status.HTTP_200_OK)
50-
51-
except Exception as e:
52-
message = f"Error retrieving usage analytics: {e}"
53-
LOG.error(message)
54-
return Response({'message': message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
55-
56-
57-
def update_usage_analytics(request) -> Response:
37+
This view allows authorized admin users to retrieve the current state of
38+
the `collect_usage` setting and update it.
5839
"""
59-
Update the user preference for analytics settings.
60-
61-
Args:
62-
request (Request): The HTTP request object containing 'analytics_enabled' parameter.
63-
64-
Returns:
65-
Response: JSON response with the updated analytics_enabled preference or an error message.
66-
67-
Raises:
68-
403: If the user is not a Hue admin.
69-
400: If 'analytics_enabled' parameter is missing or invalid.
70-
500: If there is an error updating preference.
71-
"""
72-
if not is_admin(request.user):
73-
return Response({'message': "You must be a Hue admin to access this endpoint."}, status=status.HTTP_403_FORBIDDEN)
74-
75-
try:
76-
analytics_enabled = request.POST.get('analytics_enabled')
77-
78-
if analytics_enabled is None:
79-
return Response({'message': 'Missing parameter: analytics_enabled is required.'}, status=status.HTTP_400_BAD_REQUEST)
80-
81-
settings = Settings.get_settings()
82-
settings.collect_usage = coerce_bool(analytics_enabled)
83-
settings.save()
84-
85-
return Response({'analytics_enabled': settings.collect_usage}, status=status.HTTP_200_OK)
8640

87-
except Exception as e:
88-
message = f"Error updating usage analytics: {e}"
89-
LOG.error(message)
90-
return Response({'message': message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
41+
permission_classes = [IsAdminUser]
42+
43+
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response:
44+
"""Handles GET requests to retrieve the current analytics setting."""
45+
try:
46+
settings = Settings.get_settings()
47+
data = {"collect_usage": settings.collect_usage}
48+
49+
return Response(data, status=status.HTTP_200_OK)
50+
except Exception as e:
51+
LOG.error("Error retrieving usage analytics: %s", e)
52+
return Response(
53+
{"error": "A server error occurred while retrieving usage analytics settings."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
54+
)
55+
56+
def put(self, request: Request, *args: Any, **kwargs: Any) -> Response:
57+
"""Handles PUT requests to update the analytics setting."""
58+
try:
59+
serializer = UsageAnalyticsSerializer(data=request.data)
60+
if not serializer.is_valid():
61+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
62+
63+
is_enabled = serializer.validated_data["collect_usage"]
64+
65+
settings = Settings.get_settings()
66+
settings.collect_usage = is_enabled
67+
settings.save()
68+
69+
return Response(serializer.data, status=status.HTTP_200_OK)
70+
except Exception as e:
71+
LOG.error("Error updating usage analytics: %s", e)
72+
return Response(
73+
{"error": "A server error occurred while saving the usage analytics settings."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
74+
)

apps/about/src/about/api_tests.py

Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,89 +14,110 @@
1414
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1515
# See the License for the specific language governing permissions and
1616
# limitations under the License.
17-
from unittest.mock import Mock, patch
17+
from unittest.mock import MagicMock, patch
1818

19+
import pytest
20+
from django.core.exceptions import ObjectDoesNotExist
21+
from django.urls import reverse
1922
from rest_framework import status
23+
from rest_framework.test import APIClient
2024

21-
from about.api import get_usage_analytics, update_usage_analytics
25+
from useradmin.models import User
2226

2327

24-
class TestUsageAnalyticsAPI:
25-
def test_get_usage_analytics_success(self):
26-
with patch('about.api.is_admin') as mock_is_admin:
27-
with patch('about.api.Settings.get_settings') as mock_get_settings:
28-
mock_is_admin.return_value = True
29-
mock_get_settings.return_value = Mock(collect_usage=True)
28+
@pytest.mark.django_db
29+
class TestUsageAnalyticsSettingsAPI:
30+
@pytest.fixture
31+
def api_client(self) -> APIClient:
32+
return APIClient()
3033

31-
request = Mock(method='GET', user=Mock())
32-
response = get_usage_analytics(request)
34+
@pytest.fixture
35+
def regular_user(self, db) -> User:
36+
return User.objects.create_user(username="testuser", password="")
3337

34-
assert response.status_code == status.HTTP_200_OK
35-
assert response.data == {'analytics_enabled': True}
38+
@pytest.fixture
39+
def admin_user(self, db) -> User:
40+
return User.objects.create_superuser(username="adminuser", password="")
3641

37-
def test_get_usage_analytics_unauthorized(self):
38-
with patch('about.api.is_admin') as mock_is_admin:
39-
mock_is_admin.return_value = False
42+
@pytest.fixture
43+
def analytics_settings_url(self) -> str:
44+
return reverse("api:core_usage_analytics")
4045

41-
request = Mock(method='GET', user=Mock())
42-
response = get_usage_analytics(request)
46+
@patch("desktop.auth.api_permissions.is_admin", return_value=True)
47+
@patch("about.api.Settings.get_settings")
48+
def test_get_settings_as_admin_success(self, mock_get_settings, mock_is_admin, api_client, admin_user, analytics_settings_url):
49+
mock_get_settings.return_value = MagicMock(collect_usage=True)
50+
api_client.force_authenticate(user=admin_user)
4351

44-
assert response.status_code == status.HTTP_403_FORBIDDEN
45-
assert response.data['message'] == "You must be a Hue admin to access this endpoint."
52+
response = api_client.get(analytics_settings_url)
4653

47-
def test_get_usage_analytics_error(self):
48-
with patch('about.api.is_admin') as mock_is_admin:
49-
with patch('about.api.Settings.get_settings') as mock_get_settings:
50-
mock_is_admin.return_value = True
51-
mock_get_settings.side_effect = Exception("Test error")
54+
assert response.status_code == status.HTTP_200_OK
55+
assert response.data == {"collect_usage": True}
5256

53-
request = Mock(method='GET', user=Mock())
54-
response = get_usage_analytics(request)
57+
@patch("desktop.auth.api_permissions.is_admin", return_value=False)
58+
def test_get_settings_as_non_admin_forbidden(self, mock_is_admin, api_client, regular_user, analytics_settings_url):
59+
api_client.force_authenticate(user=regular_user)
5560

56-
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
57-
assert "Error retrieving usage analytics" in response.data['message']
61+
response = api_client.get(analytics_settings_url)
5862

59-
def test_update_usage_analytics_success(self):
60-
with patch('about.api.is_admin') as mock_is_admin:
61-
with patch('about.api.Settings.get_settings') as mock_get_settings:
62-
mock_is_admin.return_value = True
63-
mock_get_settings.return_value = Mock(save=Mock())
63+
assert response.status_code == status.HTTP_403_FORBIDDEN
64+
assert response.data == {"detail": "You must be a Hue admin to perform this action."}
6465

65-
request = Mock(method='POST', user=Mock(), POST={'analytics_enabled': 'true'})
66-
response = update_usage_analytics(request)
66+
@patch("desktop.auth.api_permissions.is_admin", return_value=True)
67+
@patch("about.api.Settings.get_settings")
68+
def test_get_settings_as_admin_error(self, mock_get_settings, mock_is_admin, api_client, admin_user, analytics_settings_url):
69+
mock_get_settings.side_effect = ObjectDoesNotExist("Settings not found")
70+
api_client.force_authenticate(user=admin_user)
6771

68-
assert response.status_code == status.HTTP_200_OK
69-
assert mock_get_settings.return_value.save.called
70-
assert response.data == {'analytics_enabled': True}
72+
response = api_client.get(analytics_settings_url)
7173

72-
def test_update_usage_analytics_unauthorized(self):
73-
with patch('about.api.is_admin') as mock_is_admin:
74-
mock_is_admin.return_value = False
74+
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
75+
assert response.data == {"error": "A server error occurred while retrieving usage analytics settings."}
7576

76-
request = Mock(method='POST', user=Mock(), data={'analytics_enabled': 'true'})
77-
response = update_usage_analytics(request)
77+
@patch("desktop.auth.api_permissions.is_admin", return_value=True)
78+
@patch("about.api.Settings.get_settings")
79+
def test_put_settings_as_admin_success(self, mock_get_settings, mock_is_admin, api_client, admin_user, analytics_settings_url):
80+
mock_settings = MagicMock()
81+
mock_get_settings.return_value = mock_settings
82+
api_client.force_authenticate(user=admin_user)
83+
payload = {"collect_usage": False}
7884

79-
assert response.status_code == status.HTTP_403_FORBIDDEN
80-
assert response.data['message'] == "You must be a Hue admin to access this endpoint."
85+
response = api_client.put(analytics_settings_url, data=payload, format="json")
8186

82-
def test_update_usage_analytics_missing_param(self):
83-
with patch('about.api.is_admin') as mock_is_admin:
84-
mock_is_admin.return_value = True
87+
assert response.status_code == status.HTTP_200_OK
88+
assert response.data == payload
89+
assert mock_settings.collect_usage is False
90+
mock_settings.save.assert_called_once()
8591

86-
request = Mock(method='POST', user=Mock(), POST={})
87-
response = update_usage_analytics(request)
92+
@patch("desktop.auth.api_permissions.is_admin", return_value=False)
93+
def test_put_settings_as_non_admin_forbidden(self, mock_is_admin, api_client, regular_user, analytics_settings_url):
94+
api_client.force_authenticate(user=regular_user)
8895

89-
assert response.status_code == status.HTTP_400_BAD_REQUEST
90-
assert response.data['message'] == 'Missing parameter: analytics_enabled is required.'
96+
response = api_client.put(analytics_settings_url)
9197

92-
def test_update_usage_analytics_error(self):
93-
with patch('about.api.is_admin') as mock_is_admin:
94-
with patch('about.api.Settings.get_settings') as mock_get_settings:
95-
mock_is_admin.return_value = True
96-
mock_get_settings.side_effect = Exception("Test error")
98+
assert response.status_code == status.HTTP_403_FORBIDDEN
99+
assert response.data == {"detail": "You must be a Hue admin to perform this action."}
97100

98-
request = Mock(method='POST', user=Mock(), POST={'analytics_enabled': 'true'})
99-
response = update_usage_analytics(request)
101+
@patch("desktop.auth.api_permissions.is_admin", return_value=True)
102+
@patch("about.api.Settings.get_settings")
103+
def test_put_settings_as_admin_error(self, mock_get_settings, mock_is_admin, api_client, admin_user, analytics_settings_url):
104+
mock_get_settings.side_effect = ObjectDoesNotExist("Settings not found")
105+
api_client.force_authenticate(user=admin_user)
106+
payload = {"collect_usage": False}
100107

101-
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
102-
assert "Error updating usage analytics" in response.data['message']
108+
response = api_client.put(analytics_settings_url, data=payload, format="json")
109+
110+
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
111+
assert response.data == {"error": "A server error occurred while saving the usage analytics settings."}
112+
113+
@patch("desktop.auth.api_permissions.is_admin", return_value=True)
114+
@patch("about.api.Settings.get_settings")
115+
def test_put_settings_as_admin_missing_field(self, mock_get_settings, mock_is_admin, api_client, admin_user, analytics_settings_url):
116+
mock_get_settings.return_value = MagicMock(collect_usage=True)
117+
api_client.force_authenticate(user=admin_user)
118+
payload = {}
119+
120+
response = api_client.put(analytics_settings_url, data=payload, format="json")
121+
122+
assert response.status_code == status.HTTP_400_BAD_REQUEST
123+
assert response.data == {"collect_usage": ["This field is required."]}

apps/about/src/about/serializer.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env python
2+
# Licensed to Cloudera, Inc. under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. Cloudera, Inc. licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
from rest_framework import serializers
18+
19+
20+
class UsageAnalyticsSerializer(serializers.Serializer):
21+
"""Serializes and validates the usage analytics settings data."""
22+
23+
collect_usage = serializers.BooleanField(required=True, help_text="Indicates whether usage analytics collection is enabled.")

0 commit comments

Comments
 (0)