Skip to content

Commit

Permalink
Add 'spread mark' support (#43)
Browse files Browse the repository at this point in the history
* Add 'spread mark' support

* Add a new icon for mark spread

* Spread mark on suppression

* Fix patch mark test
  • Loading branch information
TheBloodMan49 authored Jan 23, 2025
1 parent 21d1265 commit d1601ca
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 22 deletions.
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ lib64
share
pyvenv.cfg
env
.venv/

langate/static/partners

Expand Down
5 changes: 3 additions & 2 deletions backend/langate/network/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions backend/langate/network/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<int:old>/move/<int:new>/", views.MarkMove.as_view(), name="mark-move"),
path("mark/<int:old>/spread/", views.MarkSpread.as_view(), name="mark-spread"),
path("games/", views.GameList.as_view(), name="game-list"),
path("userdevices/<int:pk>/", views.UserDeviceDetail.as_view(), name="user-device-detail"),
]
10 changes: 5 additions & 5 deletions backend/langate/network/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -40,23 +40,23 @@ 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
return random.choices(existing_marks, weights=mark_proba)[0]

# 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):
Expand Down
49 changes: 45 additions & 4 deletions backend/langate/network/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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):
"""
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -34,6 +34,7 @@ library.add(
faCircle,
faClock,
faEye,
faArrowsSplitUpAndLeft,
faEyeSlash,
faChevronDown,
faChevronUp,
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/stores/devices.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,27 @@ export const useDeviceStore = defineStore('device', () => {
}
}

async function spread_marks(oldMark: number): Promise<boolean> {
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<boolean> {
try {
const response = await axios.get<GameMark>('/network/games/', { withCredentials: true });
Expand Down Expand Up @@ -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,
Expand Down
113 changes: 104 additions & 9 deletions frontend/src/views/Management/Marks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -290,7 +312,7 @@ const submitGame = async () => {
<button
class="group rounded bg-blue-500 p-1 hover:bg-blue-600"
type="button"
@click="openModal(mark.value)"
@click="openMoveModal(mark.value)"
>
<fa-awesome-icon
icon="arrows-alt"
Expand All @@ -306,6 +328,26 @@ const submitGame = async () => {
Déplacer les appareils avec cette mark
</div>
</button>
<button
class="group rounded bg-blue-500 p-1 hover:bg-blue-600"
type="button"
@click="openSpreadModal(mark.value)"
>
<fa-awesome-icon
icon="arrows-split-up-and-left"
size="lg"
class="-scale-x-100"
/>
<div
class="pointer-events-none absolute right-[-40px] z-20 mr-10 mt-10 w-32 rounded bg-gray-800 p-2 text-xs text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"
:class="{
'bottom-8': index === (edit ? marksCopy.length - 1 : marks.length - 1),
'top-0': index !== (edit ? marksCopy.length - 1 : marks.length - 1),
}"
>
Dispatcher les appareils avec cette mark sur les autres
</div>
</button>
</div>
</template>
</td>
Expand Down Expand Up @@ -364,7 +406,7 @@ const submitGame = async () => {

<!-- Mark move Modal -->
<div
v-if="move"
v-if="showMoveModal"
class="fixed inset-0 flex items-center justify-center bg-black/50"
>
<div
Expand Down Expand Up @@ -423,7 +465,60 @@ const submitGame = async () => {
<button
class="rounded-md bg-theme-nav px-4 py-2 text-white"
type="button"
@click="move = false"
@click="showMoveModal = false"
>
Annuler
</button>
<button
class="rounded-md bg-blue-700 px-4 py-2 text-white"
type="submit"
>
Valider
</button>
</div>
</form>
</div>
</div>

<!-- Mark spread Modal -->
<div
v-if="showSpreadModal"
class="fixed inset-0 flex items-center justify-center bg-black/50"
>
<div
class="w-1/2 rounded-lg bg-zinc-800 p-4"
>
<h2
class="text-center text-2xl font-bold text-white"
>
Déplacer les appareils
</h2>
<form
class="mt-4 flex flex-col gap-4"
@submit.prevent="validateSpread"
>
<div
class="flex flex-row items-center gap-2"
>
<div>
Déplacer les appareils avec la mark
</div>
<div
class="rounded-md bg-theme-nav p-1 font-bold text-white"
>
{{ currentMark }}
</div>
<div>
sur les autres marks (selon leur priorité)
</div>
</div>
<div
class="flex justify-end gap-4"
>
<button
class="rounded-md bg-theme-nav px-4 py-2 text-white"
type="button"
@click="showSpreadModal = false"
>
Annuler
</button>
Expand Down

0 comments on commit d1601ca

Please sign in to comment.