diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 75652bed40a40b22e67755b3e0502e173ec2e999..d84944b47440997de210a258c65a6316caab63e9 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -10,6 +10,7 @@ import { getCities } from '../../actions/cities' import { getEditorCompetition, setEditorSlideId, setEditorViewId } from '../../actions/editor' import { getTypes } from '../../actions/typesAction' import { useAppDispatch, useAppSelector } from '../../hooks' +import { RichSlide } from '../../interfaces/ApiRichModels' import { renderSlideIcon } from '../../utils/renderSlideIcon' import { RemoveMenuItem } from '../admin/styledComp' import { Content, InnerContent } from '../views/styled' @@ -50,6 +51,7 @@ interface CompetitionParams { const PresentationEditorPage: React.FC = () => { const { competitionId }: CompetitionParams = useParams() const dispatch = useAppDispatch() + const [sortedSlides, setSortedSlides] = useState<RichSlide[]>([]) const activeSlideId = useAppSelector((state) => state.editor.activeSlideId) const activeViewTypeId = useAppSelector((state) => state.editor.activeViewTypeId) const competition = useAppSelector((state) => state.editor.competition) @@ -60,6 +62,10 @@ const PresentationEditorPage: React.FC = () => { dispatch(getCities()) }, []) + useEffect(() => { + setSortedSlides(competition.slides.sort((a, b) => (a.order > b.order ? 1 : -1))) + }, [competition]) + const setActiveSlideId = (id: number) => { dispatch(setEditorSlideId(id)) } @@ -111,16 +117,19 @@ const PresentationEditorPage: React.FC = () => { } const onDragEnd = async (result: DropResult) => { - // dropped outside the list - if (!result.destination) { + // dropped outside the list or same place + if (!result.destination || result.destination.index === result.source.index) { return } const draggedIndex = result.source.index - const draggedSlideId = competition.slides.find((slide) => slide.order === draggedIndex)?.id + const draggedSlideId = sortedSlides[draggedIndex].id + const slidesCopy = [...sortedSlides] + const [removed] = slidesCopy.splice(draggedIndex, 1) + slidesCopy.splice(result.destination.index, 0, removed) + setSortedSlides(slidesCopy) if (draggedSlideId) { await axios .put(`/api/competitions/${competitionId}/slides/${draggedSlideId}/order`, { order: result.destination.index }) - .then(() => dispatch(getEditorCompetition(competitionId))) .catch(console.log) } } @@ -161,26 +170,26 @@ const PresentationEditorPage: React.FC = () => { <DragDropContext onDragEnd={onDragEnd}> <Droppable droppableId="droppable"> {(provided) => ( - <div key={provided.innerRef.toString()} ref={provided.innerRef} {...provided.droppableProps}> - {competition.slides && - competition.slides.map((slide, index) => ( - <Draggable key={slide.order} draggableId={slide.id.toString()} index={index}> - {(provided, snapshot) => ( - <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> - <SlideListItem - divider - button - selected={slide.id === activeSlideId} - onClick={() => setActiveSlideId(slide.id)} - onContextMenu={(event) => handleRightClick(event, slide.id)} - > - {renderSlideIcon(slide)} - <ListItemText primary={`Sida ${slide.order + 1}`} /> - </SlideListItem> - </div> - )} - </Draggable> - ))} + <div ref={provided.innerRef} {...provided.droppableProps}> + {sortedSlides.map((slide, index) => ( + <Draggable key={slide.id} draggableId={slide.id.toString()} index={index}> + {(provided, snapshot) => ( + <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> + <SlideListItem + divider + key={slide.order} + button + selected={slide.id === activeSlideId} + onClick={() => setActiveSlideId(slide.id)} + onContextMenu={(event) => handleRightClick(event, slide.id)} + > + {renderSlideIcon(slide)} + <ListItemText primary={`Sida ${slide.id}`} /> + </SlideListItem> + </div> + )} + </Draggable> + ))} {provided.placeholder} </div> )} diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index ee9689b51ad13a95941b7b9033ec3556d33f74b6..7f322d5231f063eecd287248928d85f945469a25 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -8,7 +8,10 @@ import app.database.controller as dbc from app.apis import item_response, list_response, protect_route from app.core.dto import SlideDTO from app.core.parsers import sentinel +from app.database.controller.get import slide_count +from app.database.models import Competition from flask_restx import Resource, reqparse +from flask_restx.errors import abort api = SlideDTO.api schema = SlideDTO.schema @@ -78,26 +81,18 @@ class SlideOrder(Resource): """ Edits the specified slide order using the provided arguments. """ args = slide_parser_edit.parse_args(strict=True) - order = args.get("order") + new_order = args.get("order") item_slide = dbc.get.slide(competition_id, slide_id) - if order == item_slide.order: + if new_order == item_slide.order: return item_response(schema.dump(item_slide)) - # clamp order between 0 and max - order_count = dbc.get.slide_count(competition_id) - if order < 0: - order = 0 - elif order >= order_count - 1: - order = order_count - 1 - - # get slide at the requested order - item_slide_id = dbc.get.slide(competition_id, order) - - # switch place between them - item_slide = dbc.edit.switch_order(item_slide, item_slide_id) + if not (0 <= new_order < dbc.get.slide_count(competition_id)): + abort(codes.BAD_REQUEST, f"Cant change to invalid slide order '{new_order}'") + item_competition = dbc.get.one(Competition, competition_id) + dbc.utils.move_slides(item_competition, item_slide.order, new_order) return item_response(schema.dump(item_slide)) diff --git a/server/app/database/controller/utils.py b/server/app/database/controller/utils.py index f27ce32bae1429ec606f1b895535f3e0aea74d50..9662b08ea2e2a443955da07d07b0681bf88de0f8 100644 --- a/server/app/database/controller/utils.py +++ b/server/app/database/controller/utils.py @@ -9,25 +9,70 @@ from app.database.models import Code from flask_restx import abort -def move_slides(item_competition, start_order, end_order): - """ Changes a slide order and then arranges other affected slides. """ +def move_slides(item_competition, from_order, to_order): + """ + Move slide from from_order to to_order in item_competition. + """ + + num_slides = len(item_competition.slides) + assert 0 <= from_order < num_slides, "Invalid order to move from" + assert 0 <= to_order < num_slides, "Invalid order to move to" + + # This function is sooo terrible, someone please tell me how to update + # multiple values in the database at the same time with unique constraints. + # If you update all the values at the same time none of them will collide + # but that database doesn't know that so you have to update them to some + # other value before and then change every value back to the correct one, + # so 2 commits. + + # An example will follow the entire code to make it clear what it does + # Lets say we have 5 slides, and we want to move the slide at index 1 + # to index 4. + # We begin with a list of slides with orders [0, 1, 2, 3, 4] slides = item_competition.slides - # Move up - if start_order < end_order: - for i in range(start_order + 1, end_order): - slides[i].order -= 1 - # Move down - elif start_order > end_order: - for i in range(end_order, start_order): - slides[i].order += 1 + change = 1 if to_order < from_order else -1 + start_order = min(from_order, to_order) + end_order = max(from_order, to_order) - # start = 5, end = 1 - # 1->2, 2->3, 4->5 - # 5 = 1 + # Move slides up 100 + for item_slide in slides: + item_slide.order += 100 + + # Our slide orders now look like [100, 101, 102, 103, 104] + + # Move slides between from and to order either up or down, but minus in front + for item_slide in slides: + if start_order <= item_slide.order - 100 <= end_order: + item_slide.order = -(item_slide.order + change) + + # Our slide orders now look like [100, -100, -101, -102, -103] + + # Find the slide that was to be moved and change it to correct order with minus in front + for item_slide in slides: + if item_slide.order == -(from_order + change + 100): + item_slide.order = -(to_order + 100) + break + + # Our slide orders now look like [100, -104, -101, -102, -103] + + db.session.commit() + + # Negate all order so that they become positive + for item_slide in slides: + if start_order <= -(item_slide.order + 100) <= end_order: + item_slide.order = -(item_slide.order) + + # Our slide orders now look like [100, 104, 101, 102, 103] + + for item_slide in slides: + item_slide.order -= 100 + + # Our slide orders now look like [0, 4, 1, 2, 3] + + # We have now successfully moved slide 1 to 4 - slides[start_order].order = end_order return commit_and_refresh(item_competition) diff --git a/server/tests/test_db.py b/server/tests/test_db.py index c504ed14ff39224f9bb76fe8fd32ca99e12dd725..568c1c479fdf6f1edaaabf6765171613ae4faeb5 100644 --- a/server/tests/test_db.py +++ b/server/tests/test_db.py @@ -3,10 +3,10 @@ This file tests the database controller functions. """ import app.database.controller as dbc -from app.database.models import City, Competition, Media, MediaType, Role, User, Code +from app.database.models import City, Code, Competition, Media, MediaType, Role, User from tests import app, client, db -from tests.test_helpers import add_default_values, assert_exists, assert_insert_fail, delete +from tests.test_helpers import add_default_values, assert_exists, assert_insert_fail, assert_slide_order, delete def test_user(client): @@ -151,6 +151,33 @@ def check_slides_copy(item_slide_original, item_slide_copy, num_slides, order): assert item_slide_copy == item_slides[order] +def test_move_slides(client): + add_default_values() + + item_comp = dbc.get.one(Competition, 1) + + for _ in range(9): + dbc.add.slide(item_comp.id) + + # Move from beginning to end + item_comp = dbc.utils.move_slides(item_comp, 0, 9) + assert_slide_order(item_comp, [9, 0, 1, 2, 3, 4, 5, 6, 7, 8]) + + # Move from end to beginning + item_comp = dbc.utils.move_slides(item_comp, 9, 0) + assert_slide_order(item_comp, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + + # Move some things in the middle + item_comp = dbc.utils.move_slides(item_comp, 3, 7) + assert_slide_order(item_comp, [0, 1, 2, 7, 3, 4, 5, 6, 8, 9]) + + item_comp = dbc.utils.move_slides(item_comp, 1, 5) + assert_slide_order(item_comp, [0, 5, 1, 7, 2, 3, 4, 6, 8, 9]) + + item_comp = dbc.utils.move_slides(item_comp, 8, 2) + assert_slide_order(item_comp, [0, 6, 1, 8, 3, 4, 5, 7, 2, 9]) + + """ def test_question(client): add_default_values() diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index 85c1f114a8c01da6e7d5370bffe2ccb9ea8aed1c..7d5625385f01f81cc326bca9d5114322ad5f52e0 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -153,3 +153,11 @@ def change_order_test(client, cid, slide_id, new_slide_id, h): # Changes order response, _ = put(client, f"/api/competitions/{cid}/slides/{slide_id}/order", {"order": new_order}, headers=h) assert response.status_code == codes.OK + + +def assert_slide_order(item_comp, correct_order): + """ + Assert that the slides in the given competition are in the correct order + """ + for slide, order in zip(item_comp.slides, correct_order): + assert slide.order == order