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
2 changes: 1 addition & 1 deletion dev/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ services:
- redis
vote:
build:
context: "https://github.com/OpenSlides/openslides-vote-service.git#${COMPOSE_REFERENCE_BRANCH:-main}"
context: "https://github.com/OpenSlides/openslides-vote-service.git#feature/vote" # TODO: change branch back to the main
target: "dev"
args:
CONTEXT: "dev"
Expand Down
12 changes: 0 additions & 12 deletions openslides_backend/action/actions/meeting/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,3 @@ def get_committee_id(self, instance: dict[str, Any]) -> int:
["committee_id"],
)
return meeting["committee_id"]

def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
"""
Handle deletion of polls and all the related instances in the vote service.
"""
poll_ids = self.datastore.get(f"meeting/{instance['id']}", ["poll_ids"]).get(
"poll_ids", []
)
for poll_id in poll_ids:
self.vote_service.delete(poll_id)
self.datastore.apply_to_be_deleted(f"poll/{poll_id}")
return instance
18 changes: 17 additions & 1 deletion openslides_backend/action/generics/delete.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Iterable
from typing import Any, cast

from ...models.base import collections_managed_by_vote
from ...models.fields import BaseRelationField, OnDelete
from ...shared.exceptions import ActionException, ProtectedModelsException
from ...shared.interfaces.event import Event, EventType
Expand All @@ -27,6 +28,7 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
Takes care of on_delete handling.
"""
this_fqid = fqid_from_collection_and_id(self.model.collection, instance["id"])
poll_ids_to_delete: list[int] = []

if self.datastore.is_to_be_deleted(this_fqid):
return instance
Expand Down Expand Up @@ -55,7 +57,8 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
field = cast(BaseRelationField, self.model.get_field(field_name))
# Check on_delete.
# Extract all foreign keys as fqids from the model
foreign_fqids = transform_to_fqids(value, field.get_target_collection())
collection_from_field = field.get_target_collection()
foreign_fqids = transform_to_fqids(value, collection_from_field)
if field.on_delete != OnDelete.SET_NULL:
if field.on_delete == OnDelete.PROTECT:
protected_fqids = [
Expand All @@ -65,6 +68,17 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
]
if protected_fqids:
raise ProtectedModelsException(this_fqid, protected_fqids)
elif collection_from_field in collections_managed_by_vote:
if collection_from_field == "poll":
poll_ids_to_delete = [
id_
for id_ in value
if not self.is_to_be_deleted(
fqid_from_collection_and_id("poll", id_)
)
]
for fqid in foreign_fqids:
self.datastore.apply_to_be_deleted(fqid)
else:
# case: field.on_delete == OnDelete.CASCADE
# Execute the delete action for all fqids
Expand All @@ -91,6 +105,8 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
# Add additional relation models and execute all previously gathered delete actions
# catch all protected models exception to gather all protected fqids
all_protected_fqids: list[FullQualifiedId] = []
for id_ in poll_ids_to_delete:
self.vote_service.delete(id_)
for fqid, delete_action_class, delete_action_data in delete_actions:
try:
# Skip models that were deleted in the meantime
Expand Down
3 changes: 3 additions & 0 deletions openslides_backend/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from . import fields

model_registry: dict[Collection, type["Model"]] = {}
collections_managed_by_vote: list[Collection] = []


def json_dict_to_non_json_data_types(json: dict[str, Any]) -> None:
Expand Down Expand Up @@ -59,6 +60,8 @@ def __new__(metaclass, class_name, class_parents, class_attributes): # type: ig
attr.own_collection = new_class.collection
attr.own_field_name = attr_name
model_registry[new_class.collection] = new_class
if new_class.managed_by == "vote":
collections_managed_by_vote.append(new_class.collection)
return new_class


Expand Down
65 changes: 49 additions & 16 deletions openslides_backend/services/vote/adapter.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from enum import StrEnum
from typing import Any, Literal

import requests
Expand All @@ -9,6 +10,11 @@
from .interface import VoteService


class RequestMethod(StrEnum):
POST = "post"
DELETE = "delete"


class VoteAdapter(VoteService, AuthenticatedService):
"""
Adapter to connect to the vote service.
Expand All @@ -18,27 +24,46 @@ def __init__(self, vote_url: str, logging: LoggingModule) -> None:
self.url = vote_url
self.logger = logging.getLogger(__name__)

def retrieve(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
response = self.make_request(endpoint, payload)
def retrieve(
self,
endpoint: str,
request_method: RequestMethod = RequestMethod.POST,
payload: dict[str, Any] | None = None,
) -> Any:
response = self.make_request(endpoint, request_method, payload)
message = f"Vote service sends HTTP {response.status_code} with the following content: {str(response.content)}."
if response.status_code < 400:
self.logger.debug(message)
elif response.status_code == 500:
self.logger.error(message)
raise VoteServiceException(
"Vote service sends HTTP 500 Internal Server Error."
f"Vote service sends HTTP 500 Internal Server Error with the message: {message}."
)
else:
self.logger.error(message)
raise VoteServiceException(message)
if response.content:
return response.json()

def make_request(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
def make_request(
self,
endpoint: str,
request_method: RequestMethod,
payload: dict[str, Any] | None = None,
) -> Any:
if not self.access_token or not self.refresh_id:
raise VoteServiceException("You must be logged in to vote")
payload_json = json.dumps(payload, separators=(",", ":")) if payload else None
try:
if request_method == RequestMethod.DELETE:
return requests.delete(
url=endpoint,
headers={
"Content-Type": "application/json",
**self.get_auth_header(),
},
cookies=self.get_auth_cookie(),
)
return requests.post(
url=endpoint,
data=payload_json,
Expand All @@ -55,34 +80,42 @@ def make_request(self, endpoint: str, payload: dict[str, Any] | None = None) ->
raise VoteServiceException(f"Cannot reach the vote service on {endpoint}.")

def create(self, payload: dict[str, Any]) -> dict[str, Any]:
endpoint = self.get_endpoint("create")
return self.retrieve(endpoint, payload)
endpoint = self.get_endpoint()
return self.retrieve(endpoint, payload=payload)

def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]:
endpoint = self.get_endpoint("update", id)
return self.retrieve(endpoint, payload)
endpoint = self.get_endpoint(id)
return self.retrieve(endpoint, payload=payload)

def delete(self, id: int) -> dict[str, Any]:
endpoint = self.get_endpoint("delete", id)
return self.retrieve(endpoint)
endpoint = self.get_endpoint(id)
return self.retrieve(endpoint, RequestMethod.DELETE)

def start(self, id: int) -> dict[str, Any]:
endpoint = self.get_endpoint("start", id)
endpoint = self.get_endpoint(id, "start")
return self.retrieve(endpoint)

def finalize(
self,
id: int,
optional_attributes: list[Literal["publish", "anonymize"]] = [],
) -> dict[str, Any]:
endpoint = self.get_endpoint("finalize", id)
endpoint = self.get_endpoint(id, "finalize")
if optional_attributes:
endpoint += f"&{'&'.join(optional_attributes)}"
endpoint += f"?{'&'.join(optional_attributes)}"
return self.retrieve(endpoint)

def reset(self, id: int) -> dict[str, Any]:
endpoint = self.get_endpoint("reset", id)
endpoint = self.get_endpoint(id, "reset")
return self.retrieve(endpoint)

def get_endpoint(self, route: str, id: int | None = None) -> str:
return f"{self.url}/{route}" + (f"?id={id}" if id else "")
def vote(self, id: int, payload: dict[str, Any]) -> dict[str, Any]:
endpoint = self.get_endpoint(id, "vote")
return self.retrieve(endpoint, payload=payload)

def get_endpoint(self, id: int | None = None, route: str | None = None) -> str:
return (
f"{self.url}/poll"
+ (f"/{id}" if id else "")
+ (f"/{route}" if route else "")
)
3 changes: 3 additions & 0 deletions openslides_backend/services/vote/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ def finalize(

@abstractmethod
def reset(self, id: int) -> dict[str, Any]: ...

@abstractmethod
def vote(self, id: int, payload: dict[str, Any]) -> dict[str, Any]: ...
11 changes: 8 additions & 3 deletions tests/system/action/meeting/test_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,10 +437,15 @@ def test_delete_full_meeting(self) -> None:
self.assert_model_not_exists(f"motion_workflow/{i+1}")
for i in range(5):
self.assert_model_not_exists(f"poll/{i+1}")
for i in range(13):
self.assert_model_not_exists(f"option/{i+1}")
for i in range(10):
self.assert_model_not_exists(f"poll_option/{i+1}")
for i in range(9):
self.assert_model_not_exists(f"vote/{i+1}")
self.assert_model_not_exists(f"poll_ballot/{i+1}")
for i in range(2):
self.assert_model_not_exists(f"poll_config_approval/{i+1}")
self.assert_model_not_exists("poll_config_selection/1")
self.assert_model_not_exists("poll_config_rating_score/1")
self.assert_model_not_exists("poll_config_rating_approval/1")
for i in range(2):
self.assert_model_not_exists(f"assignment/{i+1}")
for i in range(5):
Expand Down
8 changes: 5 additions & 3 deletions tests/system/action/user/test_merge_together.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, Literal, cast
from zoneinfo import ZoneInfo

import pytest
from psycopg.types.json import Jsonb

from openslides_backend.action.actions.speaker.speech_state import SpeechState
Expand All @@ -17,11 +18,13 @@
fqid_from_collection_and_id,
)
from openslides_backend.shared.util import ONE_ORGANIZATION_ID
from tests.system.action.poll.test_vote import BaseVoteTestCase
from tests.system.action.base import BaseActionTestCase
from tests.util import Response


class UserMergeTogether(BaseVoteTestCase):
@pytest.mark.skip()
# class UserMergeTogether(BaseVoteTestCase):
class UserMergeTogether(BaseActionTestCase):
"""committee/63 is created but remains unused in all of the tests as 60 is used for meeting/1 and 4"""

def setUp(self) -> None:
Expand Down Expand Up @@ -849,7 +852,6 @@ def set_up_polls_for_merge(self) -> None:
"meeting/10": {"present_user_ids": [5]},
"meeting_user/15": {"vote_delegated_to_id": 14},
"motion_state/4": {"allow_create_poll": True},
"motion_state/4": {"allow_create_poll": True},
"motion_submitter/1": {
"id": 1,
"weight": 1,
Expand Down
Loading