diff --git a/backend/.gitignore b/backend/.gitignore index b71a0f2..5fb10cd 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -17,6 +17,7 @@ lib64 share pyvenv.cfg env +.venv/ langate/static/partners diff --git a/backend/langate/network/tests.py b/backend/langate/network/tests.py index fc06b95..5a42e1b 100644 --- a/backend/langate/network/tests.py +++ b/backend/langate/network/tests.py @@ -591,8 +591,9 @@ def test_get_marks(self, mock_settings): self.assertEqual(response.data[i]["devices"], Device.objects.filter(mark=self.settings["marks"][i]["value"], whitelisted=False).count()) self.assertEqual(response.data[i]["whitelisted"], Device.objects.filter(mark=self.settings["marks"][i]["value"], whitelisted=True).count()) + @patch('langate.settings.netcontrol.set_mark', return_value=None) @patch('langate.network.views.save_settings') - def test_patch_marks(self, mock_save_settings): + def test_patch_marks(self, mock_save_settings, mock_set_mark): mock_save_settings.side_effect = lambda x: None new_marks = [ @@ -603,7 +604,7 @@ def test_patch_marks(self, mock_save_settings): from langate.settings import SETTINGS as ORIGINAL_SETTINGS - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(ORIGINAL_SETTINGS["marks"]), 2) self.assertEqual(ORIGINAL_SETTINGS["marks"][0]["value"], 102) self.assertEqual(ORIGINAL_SETTINGS["marks"][1]["value"], 103) diff --git a/backend/langate/network/urls.py b/backend/langate/network/urls.py index 5b1de82..9948b9d 100644 --- a/backend/langate/network/urls.py +++ b/backend/langate/network/urls.py @@ -10,6 +10,7 @@ path("devices/whitelist/", views.DeviceWhitelist.as_view(), name="device-whitelist"), path("marks/", views.MarkList.as_view(), name="mark-list"), path("mark//move//", views.MarkMove.as_view(), name="mark-move"), + path("mark//spread/", views.MarkSpread.as_view(), name="mark-spread"), path("games/", views.GameList.as_view(), name="game-list"), path("userdevices//", views.UserDeviceDetail.as_view(), name="user-device-detail"), ] diff --git a/backend/langate/network/utils.py b/backend/langate/network/utils.py index a70ccd5..6266313 100644 --- a/backend/langate/network/utils.py +++ b/backend/langate/network/utils.py @@ -25,7 +25,7 @@ def generate_dev_name(): except FileNotFoundError: return "MISSINGNO" -def get_mark(user=None): +def get_mark(user=None, excluded_marks=[]): """ Get a mark from the settings based on random probability """ @@ -40,14 +40,14 @@ def get_mark(user=None): existing_marks = [ mark for mark in SETTINGS["games"][user.tournament] - if mark in [x["value"] for x in SETTINGS["marks"]] + if mark in [x["value"] for x in SETTINGS["marks"]] and mark not in excluded_marks ] mark_proba = [ mark_data["priority"] for mark in existing_marks for mark_data in SETTINGS["marks"] - if mark_data["value"] == mark + if mark_data["value"] == mark and mark_data["value"] not in excluded_marks ] # Chose a random mark from the user's tournament based on the probability @@ -55,8 +55,8 @@ def get_mark(user=None): # Get a random mark from the settings based on the probability return random.choices( - [mark["value"] for mark in SETTINGS["marks"]], - weights=[mark["priority"] for mark in SETTINGS["marks"]] + [mark["value"] for mark in SETTINGS["marks"] if mark["value"] not in excluded_marks], + weights=[mark["priority"] for mark in SETTINGS["marks"] if mark["value"] not in excluded_marks] )[0] def validate_marks(marks): diff --git a/backend/langate/network/views.py b/backend/langate/network/views.py index 41ac96c..24be005 100644 --- a/backend/langate/network/views.py +++ b/backend/langate/network/views.py @@ -17,7 +17,7 @@ from langate.settings import SETTINGS from langate.user.models import Role from langate.network.models import Device, UserDevice, DeviceManager -from langate.network.utils import validate_marks, validate_games, save_settings +from langate.network.utils import validate_marks, validate_games, save_settings, get_mark from langate.network.serializers import DeviceSerializer, UserDeviceSerializer, FullDeviceSerializer @@ -306,8 +306,11 @@ def get(self, request): def patch(self, request): """ - Create a new mark + Modify the list of marks """ + if request.data is None or len(request.data) == 0: + return Response({"error": _("No data provided")}, status=status.HTTP_400_BAD_REQUEST) + if not validate_marks(request.data): return Response({"error": _("Invalid mark")}, status=status.HTTP_400_BAD_REQUEST) @@ -320,11 +323,22 @@ def patch(self, request): "priority": mark["priority"] }) - SETTINGS["marks"] = marks + # If some marks are removed, add the new marks first, spread the devices and then remove the old marks + old_marks = [m["value"] for m in SETTINGS["marks"]] + new_marks = [m["value"] for m in marks] + removed_marks = [m for m in old_marks if m not in new_marks] + SETTINGS["marks"] = marks save_settings(SETTINGS) - return Response(SETTINGS["marks"], status=status.HTTP_201_CREATED) + if removed_marks: + for mark in removed_marks: + devices = Device.objects.filter(mark=mark) + for device in devices: + new = get_mark(excluded_marks=[mark]) + DeviceManager.edit_device(device, device.mac, device.name, new) + + return Response(SETTINGS["marks"], status=status.HTTP_200_OK) class MarkMove(APIView): """ @@ -352,6 +366,33 @@ def post(self, request, old, new): return Response(status=status.HTTP_200_OK) +class MarkSpread(APIView): + """ + API endpoint that allows all devices on a mark to be moved to another mark. + """ + + permission_classes = [StaffPermission] + + def post(self, request, old): + """ + Move all devices on a mark to another mark + """ + # Check that the old and new marks are valid + marks = [m["value"] for m in SETTINGS["marks"]] + + if old not in marks: + return Response({"error": _("Invalid origin mark")}, status=status.HTTP_400_BAD_REQUEST) + + if sum([mark["priority"] for mark in SETTINGS["marks"] if mark["value"] != old]) == 0: + return Response({"error": _("No mark to spread to")}, status=status.HTTP_400_BAD_REQUEST) + + devices = Device.objects.filter(mark=old, whitelisted=False) + for device in devices: + new = get_mark(excluded_marks=[old]) + DeviceManager.edit_device(device, device.mac, device.name, new) + + return Response(status=status.HTTP_200_OK) + class GameList(APIView): """ API endpoint that allows games to be viewed. diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 663b85b..c9b26d1 100755 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,8 +1,8 @@ /* eslint-disable no-console */ import { library } from '@fortawesome/fontawesome-svg-core'; import { - faArrowLeft, faArrowsAlt, - faArrowsRotate, faBolt, faChevronDown, faChevronUp, + faArrowLeft, faArrowsAlt, faArrowsRotate, faArrowsSplitUpAndLeft, + faBolt, faChevronDown, faChevronUp, faCircle, faCircleCheck, faCirclePlus, faClock, faCrown, faDownload, faEye, faEyeSlash, faFile, faHammer, faKey, faLocationDot, @@ -34,6 +34,7 @@ library.add( faCircle, faClock, faEye, + faArrowsSplitUpAndLeft, faEyeSlash, faChevronDown, faChevronUp, diff --git a/frontend/src/stores/devices.store.ts b/frontend/src/stores/devices.store.ts index 0d2c993..c1d799b 100644 --- a/frontend/src/stores/devices.store.ts +++ b/frontend/src/stores/devices.store.ts @@ -194,6 +194,27 @@ export const useDeviceStore = defineStore('device', () => { } } + async function spread_marks(oldMark: number): Promise { + await get_csrf(); + + try { + await axios.post(`/network/mark/${oldMark}/spread/`, {}, { + headers: { + 'X-CSRFToken': csrf.value, + 'Content-Type': 'application/json', + }, + withCredentials: true, + }); + return true; + } catch (err) { + addNotification( + (err as AxiosError<{ error?: string }>).response?.data || 'An error occurred while spreading the marks', + 'error', + ); + return false; + } + } + async function fetch_game_marks(): Promise { try { const response = await axios.get('/network/games/', { withCredentials: true }); @@ -287,6 +308,7 @@ export const useDeviceStore = defineStore('device', () => { fetch_marks, patch_marks, move_marks, + spread_marks, fetch_game_marks, change_game_marks, edit_own_device, diff --git a/frontend/src/views/Management/Marks.vue b/frontend/src/views/Management/Marks.vue index 3140b6d..fa020e6 100644 --- a/frontend/src/views/Management/Marks.vue +++ b/frontend/src/views/Management/Marks.vue @@ -11,7 +11,7 @@ const { addNotification } = useNotificationStore(); const deviceStore = useDeviceStore(); const { - fetch_marks, patch_marks, move_marks, fetch_game_marks, change_game_marks, + fetch_marks, patch_marks, move_marks, spread_marks, fetch_game_marks, change_game_marks, } = deviceStore; const { marks, gameMarks } = storeToRefs(deviceStore); @@ -50,24 +50,46 @@ const reset = async () => { edit.value = false; }; -// -- Move Modal -- +// -- Move and Spread Modals -- -const move = ref(false); const currentMark = ref(0); + +// -- Move Modal -- + +const showMoveModal = ref(false); const chosenMark = ref(0); -const openModal = (selectedMark: number) => { +const openMoveModal = (selectedMark: number) => { currentMark.value = selectedMark; // set the chosen mark to the first mark in the list chosenMark.value = marks.value[0].value; - move.value = true; + showMoveModal.value = true; }; const validateMove = async () => { if (await move_marks(currentMark.value, chosenMark.value)) { addNotification('Les appareils ont bien été déplacés', 'info'); // close the modal - move.value = false; + showMoveModal.value = false; + + // reload the marks to update the number of devices + await fetch_marks(); + } +}; + +// -- Spread Modal -- +const showSpreadModal = ref(false); + +const openSpreadModal = (selectedMark: number) => { + currentMark.value = selectedMark; + showSpreadModal.value = true; +}; + +const validateSpread = async () => { + if (await spread_marks(currentMark.value)) { + addNotification('Les appareils ont bien été déplacés', 'info'); + // close the modal + showSpreadModal.value = false; // reload the marks to update the number of devices await fetch_marks(); @@ -290,7 +312,7 @@ const submitGame = async () => { + @@ -364,7 +406,7 @@ const submitGame = async () => {
{ + +
+ +
+ + + +
+
+

+ Déplacer les appareils +

+
+
+
+ Déplacer les appareils avec la mark +
+
+ {{ currentMark }} +
+
+ sur les autres marks (selon leur priorité) +
+
+
+