Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
#### Migrations:
* 0159_alter_status_of_moderation_events_table.py - This migration alters status of api_moderationevent table.
* 0160_allow_null_parsing_errors_in_facilitylist.py - This migration allows empty parsing_errors in api_facilitylist.
* 0161_create_disable_list_uploading_switch.py - This migration creates disable_list_uploading switch in the Django admin panel and record in the waffle_switch table.

#### Scheme changes
* [OSDEV-1346](https://opensupplyhub.atlassian.net/browse/OSDEV-1346) - Alter status options for api_moderationevent table.
* [OSDEV-1411](https://opensupplyhub.atlassian.net/browse/OSDEV-1411) - Allows empty parsing_errors in api_facilitylist.

### Code/API changes
* [OSDEV-1346](https://opensupplyhub.atlassian.net/browse/OSDEV-1346) - Create GET request for `v1/moderation-events` endpoint.
* [OSDEV-1332](https://opensupplyhub.atlassian.net/browse/OSDEV-1332) - Introduced new `PATCH api/v1/moderation-events/{moderation_id}` endpoint
* [OSDEV-1429](https://opensupplyhub.atlassian.net/browse/OSDEV-1429) - The list upload switcher has been created to disable the `Submit` button on the List Contribute page through the Switch page in the Django admin panel during the release process. Implemented a check on the list upload endpoint.
* [OSDEV-1332](https://opensupplyhub.atlassian.net/browse/OSDEV-1332) - Introduced new `PATCH api/v1/moderation-events/{moderation_id}` endpoint
to modify moderation event `status`.
* [OSDEV-1347](https://opensupplyhub.atlassian.net/browse/OSDEV-1347) - Create GET request for `v1/moderation-events/{moderation_id}` endpoint.

Expand Down
10 changes: 9 additions & 1 deletion src/django/api/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from rest_framework.exceptions import APIException
from rest_framework import status


class BadRequestException(APIException):
status_code = 400
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Bad request'
default_code = 'bad_request'


class ServiceUnavailableException(APIException):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
default_detail = 'Service is temporarily unavailable due to maintenance \
work. Please try again later.'
default_code = 'service_unavailable'
1 change: 1 addition & 0 deletions src/django/api/management/commands/enable_switches.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ def handle(self, *args, **options):
call_command('waffle_switch', 'report_a_facility', 'on')
call_command('waffle_switch', 'embedded_map', 'on')
call_command('waffle_switch', 'extended_profile', 'on')
call_command('waffle_switch', 'disable_list_uploading', 'off')
26 changes: 26 additions & 0 deletions src/django/api/migrations/0161_create_bdisable_list_uploading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.17 on 2024-11-22 09:50

from django.db import migrations


def create_disable_list_uploading_switch(apps, schema_editor):
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.create(name='disable_list_uploading', active=False)


def delete_disable_list_uploading_switch(apps, schema_editor):
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.get(name='disable_list_uploading').delete()


class Migration(migrations.Migration):

dependencies = [
('api', '0160_allow_null_parsing_errors_in_facilitylist'),
]

operations = [
migrations.RunPython(
create_disable_list_uploading_switch,
delete_disable_list_uploading_switch,)
]
31 changes: 31 additions & 0 deletions src/django/api/tests/test_facility_list_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase
from waffle.testutils import override_switch

from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
Expand Down Expand Up @@ -245,3 +246,33 @@ def test_list_request_by_user_with_no_contributor_returns_400(self):
Contributor.objects.all().delete()
response = self.client.get(reverse("facility-list-list"))
self.assertEqual(response.status_code, 400)

@override_switch("disable_list_uploading", active=True)
def test_no_upload_when_disable_list_uploading_switch_active(self):
previous_list_count = FacilityList.objects.all().count()
previous_source_count = Source.objects.all().count()
expected = ["Open Supply Hub is undergoing maintenance and not \
accepting new data at the moment. Please try again in a \
few minutes."]

response = self.client.post(
reverse("facility-list-list"),
{"file": self.test_file},
format="multipart",
)

self.assertEqual(response.status_code,
status.HTTP_503_SERVICE_UNAVAILABLE)

error_message = json.loads(response.content)['detail']
expected_message = " ".join(expected[0].split())
self.assertEqual(
" ".join(error_message.split()),
expected_message
)
self.assertEqual(
FacilityList.objects.all().count(), previous_list_count
)
self.assertEqual(
Source.objects.all().count(), previous_source_count
)
7 changes: 7 additions & 0 deletions src/django/api/views/facility/facility_list_view_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import logging
from functools import reduce
from waffle import switch_is_active

from oar.settings import (
MAX_UPLOADED_FILE_SIZE_IN_BYTES,
Expand All @@ -27,6 +28,7 @@
FacilityListItemsQueryParams,
ProcessingAction,
)
from api.exceptions import ServiceUnavailableException
from ...facility_history import create_dissociate_match_change_reason
from ...mail import send_facility_list_rejection_email
from ...models.contributor.contributor import Contributor
Expand Down Expand Up @@ -89,6 +91,11 @@ def create(self, request):
"is_public": true
}
"""
if switch_is_active('disable_list_uploading'):
raise ServiceUnavailableException('Open Supply Hub is undergoing \
maintenance and not accepting new \
data at the moment. Please try again \
in a few minutes.')
if 'file' not in request.data:
raise ValidationError('No file specified.')
uploaded_file = request.data['file']
Expand Down
258 changes: 258 additions & 0 deletions src/react/src/__tests__/components/SubmitListUploadingButton.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from "react-redux";
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'

import ContributeList from '../../components/Contribute';
import { MAINTENANCE_MESSAGE } from '../../util/constants';

jest.mock('@material-ui/core/Popper', () => ({ children }) => children);
jest.mock('@material-ui/core/Portal', () => ({ children }) => children);

afterEach(() => {
jest.resetAllMocks();
});

const createMockStore = (featureFlags, baseState) => {
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

return mockStore({
...baseState,
featureFlags: {
flags: featureFlags,
fetching: false,
fetchingFeatureFlags: false
}
});
};

describe('SubmitListUploadingButton component without DISABLE_LIST_UPLOADING', () => {
const features = {
disable_list_uploading: false,
};

const user = {
id: 57658,
email: '',
isModerationMode: false,
name: '',
description: '',
website: '',
contributorType: '',
otherContributorType: '',
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
facilityLists: [],
};
const initialState = {
auth: {
user: { user },
session: { fetching: false },
},
upload: {
form: { name:'', description:'', filename:'', replaces:0 },
fetching: false,
error: null,
},
facilityLists: { facilityLists: [
{
"id": 8573,
"name": "Apparel",
"description": "Test description",
"file_name": "Template_Excel.xlsx",
"is_active": false,
"is_public": true,
"item_count": 0,
"items_url": "/api/facility-lists/8573/items/",
"statuses": [],
"status_counts": {
"UPLOADED": 1,
"PARSED": 1,
"GEOCODED": 1,
"GEOCODED_NO_RESULTS": 1,
"MATCHED": 1,
"POTENTIAL_MATCH": 1,
"CONFIRMED_MATCH": 1,
"ERROR": 1,
"ERROR_PARSING": 1,
"ERROR_GEOCODING": 1,
"ERROR_MATCHING": 1,
"DUPLICATE": 1,
"DELETED": 1,
"ITEM_REMOVED": 1
},
"contributor_id": 2371,
"created_at": "2024-01-13T10:12:05.895143Z",
"match_responsibility": "moderator",
"status": "AUTOMATIC",
"status_change_reason": "test",
"file": "/Template_Excel_KdIAiX9.xlsx",
"parsing_errors": []
},],
fetchingFacilityLists: false,},
embeddedMap: { isEmbeded:true },
fetching:false,
error: null,
fetchingFacilityLists:false,
};
const newPreloadedState = {
userHasSignedIn: true,
fetchingSessionSignIn: false,
};
const store = createMockStore(features, initialState);

const renderComponent = (props = {}) =>
render(
<Provider store={store}>
<Router>
<ContributeList {...newPreloadedState} {...props}/>
</Router>,
</Provider>
);

beforeEach(() => {
jest.clearAllMocks();
});

test("renders without crashing", () => {
renderComponent();
});

test('should render the SUBMIT Button when activeFeatureFlags NOT include DISABLE_LIST_UPLOADING',async () => {
const {getByRole} = renderComponent();

const button = getByRole('button', { name: 'SUBMIT' });

expect(button).toBeInTheDocument();
expect(button).not.toHaveAttribute('disabled');
expect(button).not.toBeDisabled();
});
});

describe('SubmitListUploadingButton component with DISABLE_LIST_UPLOADING', () => {
const features = {
extended_profile: true,
disable_list_uploading: true,
};
const user = {
id: 96565,
email: '[email protected]',
isModerationMode: true,
name: 'TestName',
description: 'test description',
website: 'https://test.pl',
contributorType: 'test type',
otherContributorType: 'new type',
currentPassword: 'pass',
newPassword: 'pass1',
confirmNewPassword: 'pass1',
facilityLists: [],
};
const initialState = {
auth: {
user: { user },
session: { fetching: false },
},
upload: {
form: { name:'List name', description:'List description', filename:'file name', replaces:1 },
fetching: false,
error: null,
},
facilityLists: { facilityLists: [
{
"id": 3648,
"name": "Clothes",
"description": "No description",
"file_name": "OSHub_Data_Template_Excel.xlsx",
"is_active": false,
"is_public": true,
"item_count": 0,
"items_url": "/api/facility-lists/3648/items/",
"statuses": [],
"status_counts": {
"UPLOADED": 0,
"PARSED": 0,
"GEOCODED": 0,
"GEOCODED_NO_RESULTS": 0,
"MATCHED": 0,
"POTENTIAL_MATCH": 0,
"CONFIRMED_MATCH": 0,
"ERROR": 0,
"ERROR_PARSING": 0,
"ERROR_GEOCODING": 0,
"ERROR_MATCHING": 0,
"DUPLICATE": 0,
"DELETED": 0,
"ITEM_REMOVED": 0
},
"contributor_id": 7742,
"created_at": "2024-01-24T11:22:05.895943Z",
"match_responsibility": "moderator",
"status": "REJECTED",
"status_change_reason": "",
"file": "/OSHub_Data_Template_Excel_KdIAiX9.xlsx",
"parsing_errors": []
},],
fetchingFacilityLists: false,},
embeddedMap: { isEmbeded:false },
fetching:false,
error: null,
fetchingFacilityLists:false,
};
const preloadedState = {
userHasSignedIn: true,
fetchingSessionSignIn: false,
};
const store = createMockStore(features, initialState);

const renderComponent = (props = {}) =>
render(
<Provider store={store}>
<Router>
<ContributeList {...preloadedState} {...props}/>
</Router>,
</Provider>
);

beforeEach(() => {
jest.clearAllMocks();
});

test('should render the disabled SUBMIT Button when activeFeatureFlags include DISABLE_LIST_UPLOADING', () => {
const {getByRole} = renderComponent();
const submitButton = getByRole('button', { name: 'SUBMIT' });

expect(submitButton).toBeInTheDocument();
expect(submitButton).toHaveAttribute('disabled');
expect(submitButton).toBeDisabled();
});

test('shows tooltip on hover SUBMIT Button', async () => {
const {getByRole} = renderComponent();
const button = getByRole('button', { name:'SUBMIT' });

expect(button).toHaveTextContent('SUBMIT');
expect(button).toBeDisabled();

const noTooltipElement = document.querySelector(`[title="${
MAINTENANCE_MESSAGE}"]`);

expect(noTooltipElement).toBeInTheDocument();
fireEvent.mouseOver(button);

const tooltip = document.querySelector('[aria-describedby^="mui-tooltip-"]');

expect(tooltip).toBeInTheDocument();
fireEvent.mouseOut(button);

const noTooltipElementAfter = document.querySelector(`[title="${
MAINTENANCE_MESSAGE}"]`);

expect(noTooltipElementAfter).toBeInTheDocument();
});
});
Loading