1792 lines
69 KiB
Python
1792 lines
69 KiB
Python
import base64
|
|
import re
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from asgiref.sync import async_to_sync
|
|
from django.conf import settings
|
|
from django.db import connections
|
|
|
|
from openslides.mediafiles.models import Mediafile
|
|
from openslides.mediafiles.views import (
|
|
mediafile_database_tablename,
|
|
use_mediafile_database,
|
|
)
|
|
from openslides.motions.models import Motion
|
|
from openslides.users.views import demo_mode_users, is_demo_mode
|
|
from openslides.utils import logging
|
|
from openslides.utils.cache import element_cache
|
|
|
|
|
|
def copy(obj, *attrs):
|
|
return {attr: obj[attr] for attr in attrs if attr in obj}
|
|
|
|
|
|
fromisoformat = getattr(datetime, "fromisoformat", None) # type: ignore
|
|
|
|
|
|
def to_unix_time(datetime_str):
|
|
if not datetime_str:
|
|
return None
|
|
if not fromisoformat:
|
|
return 0 # Only available with python >=3.7...
|
|
return int(fromisoformat(datetime_str).timestamp())
|
|
|
|
|
|
def max_or_zero(iterable):
|
|
as_list = list(iterable)
|
|
if len(as_list) == 0:
|
|
return 0
|
|
else:
|
|
return max(as_list)
|
|
|
|
|
|
COLLECTION_MAPPING = {
|
|
"agenda/item": "agenda_item",
|
|
"agenda/list-of-speakers": "list_of_speakers",
|
|
"assignments/assignment": "assignment",
|
|
"assignments/assignment-option": "option",
|
|
"assignments/assignment-poll": "poll",
|
|
"assignments/assignment-vote": "vote",
|
|
"chat/chat-group": "chat_group",
|
|
"core/countdown": "projector_countdown",
|
|
"core/projector": "projector",
|
|
"core/projector-message": "projector_message",
|
|
"mediafiles/mediafile": "mediafile",
|
|
"motions/category": "motion_category",
|
|
"motions/motion": "motion",
|
|
"motions/motion-block": "motion_block",
|
|
"motions/motion-change-recommendation": "motion_change_recommendation",
|
|
"motions/motion-comment-section": "motion_comment_section",
|
|
"motions/motion-option": "option",
|
|
"motions/motion-poll": "poll",
|
|
"motions/motion-vote": "vote",
|
|
"motions/state": "motion_state",
|
|
"motions/statute-paragraph": "motion_statute_paragraph",
|
|
"motions/workflow": "motion_workflow",
|
|
"topics/topic": "topic",
|
|
"users/group": "group",
|
|
"users/personal-note": "personal_note",
|
|
"users/user": "user",
|
|
}
|
|
|
|
|
|
PERMISSION_MAPPING = {
|
|
"agenda.can_see": "agenda_item.can_see",
|
|
"agenda.can_see_internal_items": "agenda_item.can_see_internal",
|
|
"agenda.can_manage": "agenda_item.can_manage",
|
|
"assignments.can_see": "assignment.can_see",
|
|
"assignments.can_manage": "assignment.can_manage",
|
|
"assignments.can_nominate_other": "assignment.can_nominate_other",
|
|
"assignments.can_nominate_self": "assignment.can_nominate_self",
|
|
"chat.can_manage": "chat.can_manage",
|
|
"agenda.can_see_list_of_speakers": "list_of_speakers.can_see",
|
|
"agenda.can_manage_list_of_speakers": "list_of_speakers.can_manage",
|
|
"agenda.can_be_speaker": "list_of_speakers.can_be_speaker",
|
|
"mediafiles.can_see": "mediafile.can_see",
|
|
"mediafiles.can_manage": "mediafile.can_manage",
|
|
"core.can_manage_config": "meeting.can_manage_settings",
|
|
"core.can_manage_logos_and_fonts": "meeting.can_manage_logos_and_fonts",
|
|
"core.can_see_frontpage": "meeting.can_see_frontpage",
|
|
"core.can_see_autopilot": "meeting.can_see_autopilot",
|
|
"core.can_see_livestream": "meeting.can_see_livestream",
|
|
"core.can_see_history": "meeting.can_see_history",
|
|
"motions.can_see": "motion.can_see",
|
|
"motions.can_see_internal": "motion.can_see_internal",
|
|
"motions.can_manage": "motion.can_manage",
|
|
"motions.can_manage_metadata": "motion.can_manage_metadata",
|
|
"motions.can_manage_polls": "motion.can_manage_polls",
|
|
"motions.can_create": "motion.can_create",
|
|
"motions.can_create_amendments": "motion.can_create_amendments",
|
|
"motions.can_support": "motion.can_support",
|
|
"core.can_see_projector": "projector.can_see",
|
|
"core.can_manage_projector": "projector.can_manage",
|
|
"core.can_manage_tags": "projector.can_manage",
|
|
"users.can_see_extra_data": "user.can_see_extra_data",
|
|
"users.can_see_name": "user.can_see",
|
|
"users.can_manage": "user.can_manage",
|
|
"users.can_change_password": None,
|
|
}
|
|
|
|
|
|
PERMISSION_HIERARCHIE = {
|
|
"agenda_item.can_manage": ["agenda_item.can_see_internal", "agenda_item.can_see"],
|
|
"agenda_item.can_see_internal": ["agenda_item.can_see"],
|
|
"assignment.can_manage": ["assignment.can_nominate_other", "assignment.can_see"],
|
|
"assignment.can_nominate_other": ["assignment.can_see"],
|
|
"assignment.can_nominate_self": ["assignment.can_see"],
|
|
"list_of_speakers.can_manage": ["list_of_speakers.can_see"],
|
|
"list_of_speakers.can_be_speaker": ["list_of_speakers.can_see"],
|
|
"mediafile.can_manage": ["mediafile.can_see"],
|
|
"motion.can_manage": [
|
|
"motion.can_manage_metadata",
|
|
"motion.can_manage_polls",
|
|
"motion.can_see_internal",
|
|
"motion.can_create",
|
|
"motion.can_create_amendments",
|
|
"motion.can_see",
|
|
],
|
|
"motion.can_manage_metadata": ["motion.can_see"],
|
|
"motion.can_manage_polls": ["motion.can_see"],
|
|
"motion.can_see_internal": ["motion.can_see"],
|
|
"motion.can_create": ["motion.can_see"],
|
|
"motion.can_create_amendments": ["motion.can_see"],
|
|
"motion.can_support": ["motion.can_see"],
|
|
"projector.can_manage": ["projector.can_see"],
|
|
"user.can_manage": ["user.can_see_extra_data", "user.can_see"],
|
|
"user.can_see_extra_data": ["user.can_see"],
|
|
}
|
|
|
|
|
|
PROJECTION_DEFAULT_NAME_MAPPING = {
|
|
"agenda_all_items": "agenda_all_items",
|
|
"topics": "topics",
|
|
"agenda_list_of_speakers": "list_of_speakers",
|
|
"agenda_current_list_of_speakers": "current_list_of_speakers",
|
|
"motions": "motion",
|
|
"amendments": "amendment",
|
|
"motionBlocks": "motion_block",
|
|
"assignments": "assignment",
|
|
"users": "user",
|
|
"mediafiles": "mediafile",
|
|
"messages": "projector_message",
|
|
"countdowns": "projector_countdowns",
|
|
"assignment_poll": "assignment_poll",
|
|
"motion_poll": "motion_poll",
|
|
}
|
|
|
|
|
|
class OS4ExporterException(Exception):
|
|
pass
|
|
|
|
|
|
class OS4Exporter:
|
|
def __init__(self):
|
|
self.all_data = async_to_sync(element_cache.get_all_data_list)()
|
|
self._all_data_dict = None
|
|
self.data: Any = defaultdict(dict)
|
|
self.meeting: Any = {"id": 1, "projection_ids": []}
|
|
|
|
def get_data(self):
|
|
self.modify_motion_poll_ids()
|
|
self.fill_all_data_dict()
|
|
|
|
self.set_model("meeting", self.meeting)
|
|
self.migrate_agenda_items()
|
|
self.migrate_topics()
|
|
self.migrate_list_of_speakers()
|
|
self.migrate_voting_system()
|
|
self.migrate_tags()
|
|
self.migrate_chat_groups()
|
|
self.migrate_assignments()
|
|
self.migrate_mediafiles()
|
|
self.migrate_motions()
|
|
self.migrate_motion_comment_sections()
|
|
self.migrate_motion_blocks()
|
|
self.migrate_motion_categories()
|
|
self.migrate_motion_change_recommendations()
|
|
self.migrate_motion_statute_paragraphs()
|
|
self.migrate_motion_states()
|
|
self.migrate_motion_workflows()
|
|
self.migrate_projector_messages()
|
|
self.migrate_projector_countdowns()
|
|
self.migrate_personal_notes()
|
|
self.migrate_users()
|
|
self.migrate_groups()
|
|
self.migrate_projectors()
|
|
self.migrate_meeting()
|
|
|
|
self.data["_migration_index"] = 3
|
|
return self.data
|
|
|
|
def set_model(self, collection, model):
|
|
if model["id"] in self.data[collection]:
|
|
raise OS4ExporterException(f"Tried to overwrite {collection}/{model['id']}")
|
|
self.data[collection][model["id"]] = model
|
|
|
|
def get_model(self, collection, id):
|
|
return self.data[collection][id]
|
|
|
|
def exists_model(self, collection, id):
|
|
return id in self.data[collection]
|
|
|
|
def iter_collection(self, collection):
|
|
return self.data[collection].values()
|
|
|
|
def to_list_format(self):
|
|
data = {}
|
|
for collection, models in self.data.items():
|
|
data[collection] = list(models.values())
|
|
return data
|
|
|
|
def fill_all_data_dict(self):
|
|
self._all_data_dict = {
|
|
"chat_message": {}, # not exported
|
|
}
|
|
for collection, models in self.all_data.items():
|
|
self._all_data_dict[collection] = {model["id"]: model for model in models}
|
|
|
|
def get_old_model(self, collection, id):
|
|
if not self._all_data_dict:
|
|
raise OS4ExporterException("Used too early!")
|
|
return self._all_data_dict[collection][id]
|
|
|
|
def get_collection(self, collection):
|
|
return self.all_data.get(collection, [])
|
|
|
|
def to_fqid(self, *args):
|
|
"""takes a {"collection": "..", "id": ..} dict or two params (collection, id) and converts it to an fqid"""
|
|
if len(args) == 1:
|
|
collection = args[0]["collection"]
|
|
id = args[0]["id"]
|
|
else:
|
|
collection = args[0]
|
|
id = args[1]
|
|
id = self.to_new_id(collection, id)
|
|
return f"{COLLECTION_MAPPING[collection]}/{id}"
|
|
|
|
def to_new_id(self, collection, id):
|
|
if collection == "motions/motion-poll":
|
|
id += self.motion_poll_id_offset
|
|
elif collection == "motions/motion-option":
|
|
id += self.motion_option_id_offset
|
|
elif collection == "motions/motion-vote":
|
|
id += self.motion_vote_id_offset
|
|
return id
|
|
|
|
def get_generic_reverse_relation(self, this_id, field, collections):
|
|
fqids = []
|
|
for collection in collections:
|
|
for model in self.get_collection(collection):
|
|
ids = model.get(field, [])
|
|
if this_id in ids:
|
|
fqids.append(self.to_fqid(collection, model["id"]))
|
|
return fqids
|
|
|
|
def modify_motion_poll_ids(self):
|
|
"""add max_or_zero(assignmentpoll_id) to every motion poll. The same for votes and options."""
|
|
# poll
|
|
self.motion_poll_id_offset = max_or_zero(
|
|
[x["id"] for x in self.get_collection("assignments/assignment-poll")]
|
|
)
|
|
self.motion_option_id_offset = max_or_zero(
|
|
[x["id"] for x in self.get_collection("assignments/assignment-option")]
|
|
)
|
|
self.motion_vote_id_offset = max_or_zero(
|
|
[x["id"] for x in self.get_collection("assignments/assignment-vote")]
|
|
)
|
|
|
|
for motion_poll in self.get_collection("motions/motion-poll"):
|
|
motion_poll["id"] += self.motion_poll_id_offset
|
|
|
|
for motion_option in self.get_collection("motions/motion-option"):
|
|
motion_option["id"] += self.motion_option_id_offset
|
|
motion_option["poll_id"] += self.motion_poll_id_offset
|
|
|
|
for motion_vote in self.get_collection("motions/motion-vote"):
|
|
motion_vote["id"] += self.motion_vote_id_offset
|
|
motion_vote["option_id"] += self.motion_option_id_offset
|
|
|
|
self.option_id_counter = (
|
|
max_or_zero([x["id"] for x in self.get_collection("motions/motion-option")])
|
|
+ 1
|
|
)
|
|
|
|
def migrate_agenda_items(self):
|
|
for old in self.get_collection("agenda/item"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"item_number",
|
|
"comment",
|
|
"closed",
|
|
"is_internal",
|
|
"is_hidden",
|
|
"level",
|
|
"weight",
|
|
"parent_id",
|
|
)
|
|
new["type"] = {1: "common", 2: "internal", 3: "hidden"}[old["type"]]
|
|
new["duration"] = old.get("duration", 0)
|
|
new["content_object_id"] = self.to_fqid(old["content_object"])
|
|
new["child_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("agenda/item")
|
|
if x["parent_id"] == old["id"]
|
|
]
|
|
new["tag_ids"] = old["tags_id"]
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("agenda_item", new)
|
|
|
|
def migrate_topics(self):
|
|
for old in self.get_collection("topics/topic"):
|
|
new = copy(
|
|
old, "id", "title", "text", "agenda_item_id", "list_of_speakers_id"
|
|
)
|
|
new["attachment_ids"] = old["attachments_id"]
|
|
new["tag_ids"] = []
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("topic", new)
|
|
|
|
def migrate_list_of_speakers(self):
|
|
for old in self.get_collection("agenda/list-of-speakers"):
|
|
new = copy(old, "id", "closed")
|
|
new["content_object_id"] = self.to_fqid(old["content_object"])
|
|
new["speaker_ids"] = self.create_speakers(old["speakers"], old["id"])
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("list_of_speakers", new)
|
|
|
|
def create_speakers(self, speakers, los_id):
|
|
ids = []
|
|
for old in speakers:
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"note",
|
|
"point_of_order",
|
|
"user_id",
|
|
"weight",
|
|
)
|
|
new["begin_time"] = to_unix_time(old["begin_time"])
|
|
new["end_time"] = to_unix_time(old["end_time"])
|
|
if old["marked"]:
|
|
new["speech_state"] = "contribution"
|
|
elif old["pro_speech"] is True:
|
|
new["speech_state"] = "pro"
|
|
elif old["pro_speech"] is False:
|
|
new["speech_state"] = "contra"
|
|
else:
|
|
new["speech_state"] = None
|
|
new["list_of_speakers_id"] = los_id
|
|
new["meeting_id"] = 1
|
|
ids.append(old["id"])
|
|
self.set_model("speaker", new)
|
|
return ids
|
|
|
|
def migrate_voting_system(self):
|
|
# reverse relations option/vote_ids and poll/option_ids are calculated at the end.
|
|
self.migrate_votes("assignments/assignment-vote")
|
|
self.migrate_votes("motions/motion-vote")
|
|
self.migrate_options("assignments/assignment-option")
|
|
self.migrate_options("motions/motion-option")
|
|
self.migrate_polls("assignments/assignment-poll")
|
|
self.migrate_polls("motions/motion-poll")
|
|
# motion polls
|
|
self.move_votes_to_global_options()
|
|
self.calculate_poll_reverse_relations()
|
|
|
|
def migrate_votes(self, collection):
|
|
for old in self.get_collection(collection):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"weight",
|
|
"value",
|
|
"user_token",
|
|
"option_id",
|
|
"user_id",
|
|
"delegated_user_id",
|
|
)
|
|
new["meeting_id"] = 1
|
|
self.set_model("vote", new)
|
|
|
|
def migrate_options(self, collection):
|
|
for old in self.get_collection(collection):
|
|
new = copy(old, "id", "yes", "no", "abstain", "poll_id")
|
|
if "assignment" in collection:
|
|
new["content_object_id"] = self.to_fqid("users/user", old["user_id"])
|
|
else: # motion
|
|
poll = self.get_old_model("motions/motion-poll", old["poll_id"])
|
|
new["content_object_id"] = self.to_fqid(
|
|
"motions/motion", poll["motion_id"]
|
|
)
|
|
new["text"] = None
|
|
new["weight"] = old.get("weight", 1) # not defined for motion options
|
|
new["used_as_global_option_in_poll_id"] = None
|
|
new["meeting_id"] = 1
|
|
self.set_model("option", new)
|
|
|
|
def migrate_polls(self, collection):
|
|
for old in self.get_collection(collection):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"title",
|
|
"type",
|
|
"is_pseudoanonymized",
|
|
"pollmethod",
|
|
"onehundred_percent_base",
|
|
"votesvalid",
|
|
"votesinvalid",
|
|
"votescast",
|
|
"entitled_users_at_stop",
|
|
)
|
|
new["state"] = {1: "created", 2: "started", 3: "finished", 4: "published"}[
|
|
old["state"]
|
|
]
|
|
if "assignment" in collection:
|
|
new["content_object_id"] = self.to_fqid(
|
|
"assignments/assignment", old["assignment_id"]
|
|
)
|
|
else: # motion
|
|
new["content_object_id"] = self.to_fqid(
|
|
"motions/motion", old["motion_id"]
|
|
)
|
|
|
|
# these fields are not set by motion polls.
|
|
new["description"] = old.get("description", "")
|
|
new["min_votes_amount"] = old.get("min_votes_amount", 1)
|
|
new["max_votes_amount"] = old.get("max_votes_amount", 1)
|
|
new["global_yes"] = old.get("global_yes", False)
|
|
new["global_no"] = old.get("global_no", False)
|
|
new["global_abstain"] = old.get("global_abstain", False)
|
|
|
|
new["entitled_group_ids"] = old["groups_id"]
|
|
new["backend"] = "fast"
|
|
new["voted_ids"] = old["voted_id"]
|
|
new["global_option_id"] = self.create_global_option(old)
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("poll", new)
|
|
|
|
def create_global_option(self, poll):
|
|
id = self.option_id_counter
|
|
self.option_id_counter += 1
|
|
option = {
|
|
"id": id,
|
|
"weight": 1,
|
|
"text": None,
|
|
"yes": poll.get("amount_global_yes", "0.000000"),
|
|
"no": poll.get("amount_global_no", "0.000000"),
|
|
"abstain": poll.get("amount_global_abstain", "0.000000"),
|
|
"poll_id": None,
|
|
"used_as_global_option_in_poll_id": poll["id"],
|
|
"vote_ids": [],
|
|
"content_object_id": None,
|
|
"meeting_id": 1,
|
|
}
|
|
self.set_model("option", option)
|
|
return id
|
|
|
|
def move_votes_to_global_options(self):
|
|
for vote in self.iter_collection("vote"):
|
|
option = self.get_model("option", vote["option_id"])
|
|
poll = self.get_model("poll", option["poll_id"])
|
|
if vote["value"] not in poll["pollmethod"]:
|
|
# this vote is not valied for the method -> it must be a global vote.
|
|
# remove this vote from this option and add it to the global one.
|
|
# Do not care about the reverse relations - they are done later.
|
|
vote["option_id"] = poll["global_option_id"]
|
|
|
|
def calculate_poll_reverse_relations(self):
|
|
# poll/option_ids
|
|
for poll in self.iter_collection("poll"):
|
|
poll["option_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("option")
|
|
if x["poll_id"] == poll["id"]
|
|
]
|
|
# option/vote_ids
|
|
for option in self.iter_collection("option"):
|
|
option["vote_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("vote")
|
|
if x["option_id"] == option["id"]
|
|
]
|
|
|
|
def migrate_tags(self):
|
|
for old in self.get_collection("core/tag"):
|
|
new = copy(old, "id", "name")
|
|
new["tagged_ids"] = self.get_generic_reverse_relation(
|
|
old["id"],
|
|
"tags_id",
|
|
(
|
|
"agenda/item",
|
|
"topics/topic",
|
|
"motions/motion",
|
|
"assignments/assignment",
|
|
),
|
|
)
|
|
new["meeting_id"] = 1
|
|
self.set_model("tag", new)
|
|
|
|
def migrate_chat_groups(self):
|
|
for old in self.get_collection("chat/chat-group"):
|
|
new = copy(old, "id", "name")
|
|
new["weight"] = old["id"]
|
|
new["read_group_ids"] = old["read_groups_id"]
|
|
new["write_group_ids"] = old["write_groups_id"]
|
|
new["meeting_id"] = 1
|
|
new["chat_message_ids"] = []
|
|
self.set_model("chat_group", new)
|
|
|
|
def migrate_assignments(self):
|
|
for old in self.get_collection("assignments/assignment"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"title",
|
|
"description",
|
|
"open_posts",
|
|
"default_poll_description",
|
|
"number_poll_candidates",
|
|
"agenda_item_id",
|
|
"list_of_speakers_id",
|
|
)
|
|
new["phase"] = {0: "search", 1: "voting", 2: "finished"}[old["phase"]]
|
|
new["candidate_ids"] = self.create_assignment_candidates(
|
|
old["assignment_related_users"], old["id"]
|
|
)
|
|
new["poll_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("poll")
|
|
if x["content_object_id"] == f"assignment/{old['id']}"
|
|
]
|
|
new["attachment_ids"] = old["attachments_id"]
|
|
new["tag_ids"] = old["tags_id"]
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("assignment", new)
|
|
|
|
def create_assignment_candidates(self, assignment_candidates, assignment_id):
|
|
ids = []
|
|
for old in assignment_candidates:
|
|
new = copy(old, "id", "weight", "user_id")
|
|
new["assignment_id"] = assignment_id
|
|
new["meeting_id"] = 1
|
|
ids.append(old["id"])
|
|
self.set_model("assignment_candidate", new)
|
|
return ids
|
|
|
|
def migrate_mediafiles(self):
|
|
for old in self.get_collection("mediafiles/mediafile"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"title",
|
|
"is_directory",
|
|
"mimetype",
|
|
"pdf_information",
|
|
"parent_id",
|
|
"list_of_speakers_id",
|
|
)
|
|
|
|
mediafile_blob_data = self.get_mediafile_blob_data(old)
|
|
if not mediafile_blob_data:
|
|
new["filename"] = old["title"]
|
|
new["filesize"] = 0
|
|
new["blob"] = None
|
|
else:
|
|
new["filename"], new["filesize"], new["blob"] = mediafile_blob_data
|
|
|
|
new["create_timestamp"] = to_unix_time(old["create_timestamp"])
|
|
|
|
new["access_group_ids"] = old["access_groups_id"]
|
|
new["is_public"] = old["inherited_access_groups_id"] is True
|
|
inherited_access_groups_id = old["inherited_access_groups_id"]
|
|
if inherited_access_groups_id in (True, False):
|
|
new["inherited_access_group_ids"] = []
|
|
else:
|
|
new["inherited_access_group_ids"] = inherited_access_groups_id
|
|
new["child_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("mediafiles/mediafile")
|
|
if x["parent_id"] == old["id"]
|
|
]
|
|
new["attachment_ids"] = self.get_generic_reverse_relation(
|
|
old["id"],
|
|
"attachments_id",
|
|
(
|
|
"topics/topic",
|
|
"motions/motion",
|
|
"assignments/assignment",
|
|
),
|
|
)
|
|
new["projection_ids"] = []
|
|
|
|
# will be set when migrating the meeting
|
|
new["used_as_logo_$_in_meeting_id"] = []
|
|
new["used_as_font_$_in_meeting_id"] = []
|
|
|
|
new["meeting_id"] = 1
|
|
self.set_model("mediafile", new)
|
|
|
|
def get_mediafile_blob_data(self, old):
|
|
"""
|
|
Returns the tuple (filename, filesize, blob) with blob being base64 encoded
|
|
in a string. If there is an error or no mediafile, None is returned.
|
|
"""
|
|
if old["is_directory"]:
|
|
return None
|
|
|
|
try:
|
|
db_mediafile = Mediafile.objects.get(pk=old["id"])
|
|
except Mediafile.DoesNotExist:
|
|
return None
|
|
filename = db_mediafile.original_filename
|
|
|
|
if use_mediafile_database:
|
|
with connections["mediafiles"].cursor() as cursor:
|
|
cursor.execute(
|
|
f"SELECT data FROM {mediafile_database_tablename} WHERE id = %s",
|
|
[old["id"]],
|
|
)
|
|
row = cursor.fetchone()
|
|
if row is None:
|
|
return None
|
|
data = row[0]
|
|
else:
|
|
data = db_mediafile.mediafile.open().read()
|
|
|
|
blob = base64.b64encode(data).decode("utf-8")
|
|
return filename, len(data), blob
|
|
|
|
def migrate_motions(self):
|
|
recommendation_reference_motion_ids_regex = re.compile(
|
|
r"\[motion:(?P<id>\d+)\]"
|
|
)
|
|
|
|
db_number_values = {}
|
|
for motion in Motion.objects.all():
|
|
db_number_values[motion.id] = motion.identifier_number
|
|
for old in self.get_collection("motions/motion"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"title",
|
|
"text",
|
|
"modified_final_version",
|
|
"reason",
|
|
"category_weight",
|
|
"state_extension",
|
|
"recommendation_extension",
|
|
"sort_weight",
|
|
"state_id",
|
|
"recommendation_id",
|
|
"category_id",
|
|
"statute_paragraph_id",
|
|
"agenda_item_id",
|
|
"list_of_speakers_id",
|
|
)
|
|
new["number"] = old["identifier"]
|
|
new["number_value"] = db_number_values[old["id"]]
|
|
new["sequential_number"] = old["id"]
|
|
new["amendment_paragraph_$"] = []
|
|
if old["amendment_paragraphs"]:
|
|
for i, content in enumerate(old["amendment_paragraphs"]):
|
|
new["amendment_paragraph_$"].append(str(i + 1))
|
|
new[f"amendment_paragraph_${i+1}"] = content
|
|
new["sort_weight"] = old["weight"]
|
|
new["created"] = to_unix_time(old["created"])
|
|
new["last_modified"] = to_unix_time(old["last_modified"])
|
|
|
|
new["lead_motion_id"] = old["parent_id"]
|
|
new["amendment_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/motion")
|
|
if x["parent_id"] == old["id"]
|
|
]
|
|
new["sort_parent_id"] = old["sort_parent_id"]
|
|
new["sort_child_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/motion")
|
|
if x["sort_parent_id"] == old["id"]
|
|
]
|
|
new["origin_id"] = None
|
|
new["derived_motion_ids"] = []
|
|
new["all_origin_ids"] = []
|
|
new["all_derived_motion_ids"] = []
|
|
new["block_id"] = old["motion_block_id"]
|
|
new["submitter_ids"] = self.create_motion_submitters(old["submitters"])
|
|
new["supporter_ids"] = old["supporters_id"]
|
|
new["poll_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("poll")
|
|
if x["content_object_id"] == f"motion/{old['id']}"
|
|
]
|
|
new["option_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("option")
|
|
if x["content_object_id"] == f"motion/{old['id']}"
|
|
]
|
|
new["change_recommendation_ids"] = old["change_recommendations_id"]
|
|
new["comment_ids"] = self.create_motion_comments(old["comments"], old["id"])
|
|
new["tag_ids"] = old["tags_id"]
|
|
new["attachment_ids"] = old["attachments_id"]
|
|
new[
|
|
"personal_note_ids"
|
|
] = [] # will be filled later while migrating personal notes
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
|
|
new["recommendation_extension_reference_ids"] = []
|
|
if new["recommendation_extension"]:
|
|
|
|
def replace_fn(matchobj):
|
|
id = int(matchobj.group("id"))
|
|
new["recommendation_extension_reference_ids"].append(f"motion/{id}")
|
|
return f"[motion/{id}]"
|
|
|
|
new[
|
|
"recommendation_extension"
|
|
] = recommendation_reference_motion_ids_regex.sub(
|
|
replace_fn, new["recommendation_extension"]
|
|
)
|
|
|
|
self.set_model("motion", new)
|
|
|
|
for motion in self.iter_collection("motion"):
|
|
motion["referenced_in_motion_recommendation_extension_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("motion")
|
|
if f"motion/{motion['id']}"
|
|
in x["recommendation_extension_reference_ids"]
|
|
]
|
|
|
|
def create_motion_submitters(self, submitters):
|
|
ids = []
|
|
for old in submitters:
|
|
new = copy(old, "id", "motion_id", "weight", "user_id")
|
|
new["meeting_id"] = 1
|
|
ids.append(old["id"])
|
|
self.set_model("motion_submitter", new)
|
|
return ids
|
|
|
|
def create_motion_comments(self, comments, motion_id):
|
|
ids = []
|
|
for old in comments:
|
|
new = copy(old, "id", "section_id", "comment")
|
|
new["motion_id"] = motion_id
|
|
new["meeting_id"] = 1
|
|
ids.append(old["id"])
|
|
self.set_model("motion_comment", new)
|
|
return ids
|
|
|
|
def migrate_motion_comment_sections(self):
|
|
for old in self.get_collection("motions/motion-comment-section"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"name",
|
|
"weight",
|
|
)
|
|
new["read_group_ids"] = old["read_groups_id"]
|
|
new["write_group_ids"] = old["write_groups_id"]
|
|
new["comment_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("motion_comment")
|
|
if x["section_id"] == old["id"]
|
|
]
|
|
new["meeting_id"] = 1
|
|
self.set_model("motion_comment_section", new)
|
|
|
|
def migrate_motion_blocks(self):
|
|
for old in self.get_collection("motions/motion-block"):
|
|
new = copy(
|
|
old, "id", "title", "internal", "agenda_item_id", "list_of_speakers_id"
|
|
)
|
|
new["motion_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/motion")
|
|
if x["motion_block_id"] == old["id"]
|
|
]
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("motion_block", new)
|
|
|
|
def migrate_motion_categories(self):
|
|
for old in self.get_collection("motions/category"):
|
|
new = copy(old, "id", "name", "prefix", "weight", "level", "parent_id")
|
|
|
|
new["child_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/category")
|
|
if x["parent_id"] == old["id"]
|
|
]
|
|
new["motion_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/motion")
|
|
if x["category_id"] == old["id"]
|
|
]
|
|
new["meeting_id"] = 1
|
|
self.set_model("motion_category", new)
|
|
|
|
def migrate_motion_change_recommendations(self):
|
|
for old in self.get_collection("motions/motion-change-recommendation"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"rejected",
|
|
"internal",
|
|
"other_description",
|
|
"line_from",
|
|
"line_to",
|
|
"text",
|
|
"motion_id",
|
|
)
|
|
new["type"] = {0: "replacement", 1: "insertion", 2: "deletion", 3: "other"}[
|
|
old["type"]
|
|
]
|
|
new["creation_time"] = to_unix_time(old["creation_time"])
|
|
new["meeting_id"] = 1
|
|
self.set_model("motion_change_recommendation", new)
|
|
|
|
def migrate_motion_statute_paragraphs(self):
|
|
for old in self.get_collection("motions/statute-paragraph"):
|
|
new = copy(old, "id", "title", "text", "weight")
|
|
new["motion_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/motion")
|
|
if x["statute_paragraph_id"] == old["id"]
|
|
]
|
|
new["meeting_id"] = 1
|
|
self.set_model("motion_statute_paragraph", new)
|
|
|
|
def migrate_motion_states(self):
|
|
for old in self.get_collection("motions/state"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"name",
|
|
"recommendation_label",
|
|
"allow_support",
|
|
"allow_create_poll",
|
|
"allow_submitter_edit",
|
|
"show_state_extension_field",
|
|
"show_recommendation_extension_field",
|
|
"workflow_id",
|
|
)
|
|
if old["css_class"] in (
|
|
"grey",
|
|
"red",
|
|
"green",
|
|
"lightblue",
|
|
"yellow",
|
|
):
|
|
new["css_class"] = old["css_class"]
|
|
else:
|
|
new["css_class"] = "lightblue"
|
|
new["weight"] = old["id"]
|
|
|
|
new["restrictions"] = []
|
|
restrictions_map = {
|
|
"motions.can_see_internal": "motion.can_see_internal",
|
|
"motions.can_manage_metadata": "motion.can_manage_metadata",
|
|
"motions.can_manage": "motion.can_manage",
|
|
"managers_only": "motion.can_manage", # Should not exist any more since migration 0026, but does anyway...
|
|
"is_submitter": "is_submitter",
|
|
}
|
|
for restriction in old["restriction"]:
|
|
if restriction in restrictions_map:
|
|
new["restrictions"].append(restrictions_map[restriction])
|
|
else:
|
|
logging.getLogger(__name__).warn(
|
|
f"Invalid restriction '{restriction}' for motion {old['id']} is ignored."
|
|
)
|
|
|
|
new["set_number"] = not old["dont_set_identifier"]
|
|
new["merge_amendment_into_final"] = {
|
|
-1: "do_not_merge",
|
|
0: "undefined",
|
|
1: "do_merge",
|
|
}[old["merge_amendment_into_final"]]
|
|
|
|
new["next_state_ids"] = old["next_states_id"]
|
|
new["previous_state_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/state")
|
|
if old["id"] in x["next_states_id"]
|
|
]
|
|
new["motion_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/motion")
|
|
if x["state_id"] == old["id"]
|
|
]
|
|
new["motion_recommendation_ids"] = [
|
|
x["id"]
|
|
for x in self.get_collection("motions/motion")
|
|
if x["recommendation_id"] == old["id"]
|
|
]
|
|
new[
|
|
"first_state_of_workflow_id"
|
|
] = None # will be set when migrating workflows.
|
|
new["meeting_id"] = 1
|
|
self.set_model("motion_state", new)
|
|
|
|
def migrate_motion_workflows(self):
|
|
for old in self.get_collection("motions/workflow"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"name",
|
|
"first_state_id",
|
|
)
|
|
new["state_ids"] = old["states_id"]
|
|
first_state = self.get_model("motion_state", old["first_state_id"])
|
|
first_state["first_state_of_workflow_id"] = old["id"]
|
|
# the following three will be set when migrating the meeting.
|
|
new["default_workflow_meeting_id"] = None
|
|
new["default_amendment_workflow_meeting_id"] = None
|
|
new["default_statute_amendment_workflow_meeting_id"] = None
|
|
new["meeting_id"] = 1
|
|
self.set_model("motion_workflow", new)
|
|
|
|
def migrate_projector_messages(self):
|
|
for old in self.get_collection("core/projector-message"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"message",
|
|
)
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("projector_message", new)
|
|
|
|
def migrate_projector_countdowns(self):
|
|
for old in self.get_collection("core/countdown"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"title",
|
|
"description",
|
|
"default_time",
|
|
"countdown_time",
|
|
"running",
|
|
)
|
|
new["used_as_list_of_speaker_countdown_meeting_id"] = None
|
|
new["used_as_poll_countdown_meeting_id"] = None
|
|
new["projection_ids"] = []
|
|
new["meeting_id"] = 1
|
|
self.set_model("projector_countdown", new)
|
|
|
|
# Create two new countdowns: A LOS and a poll countdown
|
|
max_countdown_id = max_or_zero(
|
|
x["id"] for x in self.iter_collection("projector_countdown")
|
|
)
|
|
los_countdown = {
|
|
"id": max_countdown_id + 1,
|
|
"title": "list of speakers countdown",
|
|
"description": "created at the migration from OS3 to OS4",
|
|
"default_time": 60,
|
|
"countdown_time": 60,
|
|
"running": False,
|
|
"used_as_list_of_speaker_countdown_meeting_id": 1,
|
|
"used_as_poll_countdown_meeting_id": None,
|
|
"projection_ids": [],
|
|
"meeting_id": 1,
|
|
}
|
|
self.set_model("projector_countdown", los_countdown)
|
|
self.meeting["list_of_speakers_countdown_id"] = max_countdown_id + 1
|
|
|
|
poll_countdown = {
|
|
"id": max_countdown_id + 2,
|
|
"title": "poll countdown",
|
|
"description": "created at the migration from OS3 to OS4",
|
|
"default_time": 60,
|
|
"countdown_time": 60,
|
|
"running": False,
|
|
"used_as_list_of_speaker_countdown_meeting_id": None,
|
|
"used_as_poll_countdown_meeting_id": 1,
|
|
"projection_ids": [],
|
|
"meeting_id": 1,
|
|
}
|
|
self.set_model("projector_countdown", poll_countdown)
|
|
self.meeting["poll_countdown_id"] = max_countdown_id + 2
|
|
|
|
def migrate_personal_notes(self):
|
|
id_counter = 1
|
|
for old in self.get_collection("users/personal-note"):
|
|
notes = old.get("notes", {}).get("motions/motion", {})
|
|
for motion_id, note in notes.items():
|
|
motion_id = int(motion_id)
|
|
if not self.exists_model("motion", motion_id) or not isinstance(
|
|
note.get("note"), str
|
|
):
|
|
continue
|
|
|
|
new = {
|
|
"id": id_counter,
|
|
"user_id": old["user_id"],
|
|
"content_object_id": f"motion/{motion_id}",
|
|
"note": note["note"],
|
|
"star": note.get("star", False),
|
|
"meeting_id": 1,
|
|
}
|
|
motion = self.get_model("motion", motion_id)
|
|
motion["personal_note_ids"].append(id_counter)
|
|
self.set_model("personal_note", new)
|
|
id_counter += 1
|
|
|
|
def migrate_users(self):
|
|
for old in self.get_collection("users/user"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"username",
|
|
"title",
|
|
"first_name",
|
|
"last_name",
|
|
"is_active",
|
|
"default_password",
|
|
"gender",
|
|
"email",
|
|
)
|
|
# remove invalid genders
|
|
if new["gender"] not in ("male", "female", "diverse"):
|
|
new["gender"] = None
|
|
|
|
new["is_physical_person"] = not old["is_committee"]
|
|
new["password"] = ""
|
|
new["default_number"] = old["number"]
|
|
new["default_structure_level"] = old["structure_level"]
|
|
new["default_vote_weight"] = old["vote_weight"]
|
|
new["last_email_send"] = to_unix_time(old["last_email_send"])
|
|
|
|
new["is_demo_user"] = is_demo_mode and old["id"] in demo_mode_users
|
|
new["organization_management_level"] = None
|
|
new["is_present_in_meeting_ids"] = []
|
|
if old["is_present"]:
|
|
new["is_present_in_meeting_ids"].append(1)
|
|
new["committee_ids"] = []
|
|
new["committee_$_management_level"] = []
|
|
new["comment_$"] = []
|
|
new["number_$"] = []
|
|
new["structure_level_$"] = []
|
|
new["about_me_$"] = []
|
|
new["vote_weight_$"] = []
|
|
|
|
group_ids = old["groups_id"] or [
|
|
1
|
|
] # explicitly put users ion the default group if they do not have a group.
|
|
self.set_template(new, "group_$_ids", group_ids)
|
|
# check for permission
|
|
new["can_change_own_password"] = False
|
|
for group_id in group_ids:
|
|
group = self.get_old_model("users/group", group_id)
|
|
if group_id == 2 or "users.can_change_password" in group["permissions"]:
|
|
new["can_change_own_password"] = True
|
|
break
|
|
|
|
self.set_template(
|
|
new,
|
|
"speaker_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("speaker")
|
|
if old["id"] == x["user_id"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"personal_note_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("personal_note")
|
|
if old["id"] == x["user_id"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"supported_motion_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("motion")
|
|
if old["id"] in x["supporter_ids"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"submitted_motion_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("motion_submitter")
|
|
if old["id"] == x["user_id"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"poll_voted_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("poll")
|
|
if old["id"] in x["voted_ids"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"option_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("option")
|
|
if f"user/{old['id']}" == x["content_object_id"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"vote_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("vote")
|
|
if old["id"] == x["user_id"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"vote_delegated_vote_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("vote")
|
|
if old["id"] == x["delegated_user_id"]
|
|
],
|
|
)
|
|
self.set_template(
|
|
new,
|
|
"assignment_candidate_$_ids",
|
|
[
|
|
x["id"]
|
|
for x in self.iter_collection("assignment_candidate")
|
|
if old["id"] == x["user_id"]
|
|
],
|
|
)
|
|
new["projection_$_ids"] = []
|
|
self.set_template(
|
|
new, "vote_delegated_$_to_id", old["vote_delegated_to_id"]
|
|
)
|
|
self.set_template(
|
|
new, "vote_delegations_$_from_ids", old["vote_delegated_from_users_id"]
|
|
)
|
|
new["meeting_ids"] = [1]
|
|
new["chat_message_$_ids"] = []
|
|
|
|
self.set_model("user", new)
|
|
|
|
def set_template(self, obj, field, value):
|
|
if value:
|
|
obj[field] = ["1"]
|
|
parts = field.split("$")
|
|
obj[f"{parts[0]}$1{parts[1]}"] = value
|
|
else:
|
|
obj[field] = []
|
|
|
|
def migrate_groups(self):
|
|
# important to do after users since the reverse relation to users depends on their migration.
|
|
for old in self.get_collection("users/group"):
|
|
new = copy(old, "id", "name")
|
|
new["permissions"] = self.migrate_permissions(old["permissions"])
|
|
|
|
new["user_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("user")
|
|
if old["id"] in x["group_$1_ids"]
|
|
]
|
|
new["default_group_for_meeting_id"] = (
|
|
1 if old["id"] == 1 else None
|
|
) # default group
|
|
new["admin_group_for_meeting_id"] = (
|
|
1 if old["id"] == 2 else None
|
|
) # admin group
|
|
new["mediafile_access_group_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("mediafile")
|
|
if old["id"] in x["access_group_ids"]
|
|
]
|
|
new["mediafile_inherited_access_group_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("mediafile")
|
|
if old["id"] in x["inherited_access_group_ids"]
|
|
]
|
|
new["read_comment_section_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("motion_comment_section")
|
|
if old["id"] in x["read_group_ids"]
|
|
]
|
|
new["write_comment_section_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("motion_comment_section")
|
|
if old["id"] in x["write_group_ids"]
|
|
]
|
|
new["read_chat_group_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("chat_group")
|
|
if old["id"] in x["read_group_ids"]
|
|
]
|
|
new["write_chat_group_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("chat_group")
|
|
if old["id"] in x["write_group_ids"]
|
|
]
|
|
new["poll_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("poll")
|
|
if old["id"] in x["entitled_group_ids"]
|
|
]
|
|
new[
|
|
"used_as_motion_poll_default_id"
|
|
] = None # Next 3 are set by meeting migrations
|
|
new["used_as_assignment_poll_default_id"] = None
|
|
new["used_as_poll_default_id"] = None
|
|
new["meeting_id"] = 1
|
|
self.set_model("group", new)
|
|
self.meeting["default_group_id"] = 1
|
|
self.meeting["admin_group_id"] = 2
|
|
|
|
def migrate_permissions(self, perms):
|
|
# Note that poll.can_manage is not added to any group since
|
|
# stand-alone polls do not exist in OS3.
|
|
perms = [
|
|
PERMISSION_MAPPING[x] for x in perms if PERMISSION_MAPPING[x] is not None
|
|
]
|
|
new_perms = set(perms)
|
|
for perm in perms:
|
|
new_perms -= set(PERMISSION_HIERARCHIE.get(perm, []))
|
|
return list(new_perms)
|
|
|
|
def migrate_projectors(self):
|
|
self.projection_id_counter = 1
|
|
for old in self.get_collection("core/projector"):
|
|
new = copy(
|
|
old,
|
|
"id",
|
|
"name",
|
|
"scale",
|
|
"scroll",
|
|
"width",
|
|
"aspect_ratio_numerator",
|
|
"aspect_ratio_denominator",
|
|
"color",
|
|
"background_color",
|
|
"header_background_color",
|
|
"header_font_color",
|
|
"header_h1_color",
|
|
"chyron_background_color",
|
|
"chyron_font_color",
|
|
"show_header_footer",
|
|
"show_title",
|
|
"show_logo",
|
|
)
|
|
new["show_clock"] = False
|
|
|
|
new["current_projection_ids"] = []
|
|
new["preview_projection_ids"] = []
|
|
new["history_projection_ids"] = []
|
|
|
|
for i, element in enumerate(old["elements"]):
|
|
if element["name"] == "core/clock":
|
|
new["show_clock"] = True
|
|
continue
|
|
projection_id = self.create_projection_from_projector_element(
|
|
element, i + 1, "current", old["id"]
|
|
)
|
|
if projection_id > 0:
|
|
new["current_projection_ids"].append(projection_id)
|
|
|
|
for i, element in enumerate(old["elements_preview"]):
|
|
projection_id = self.create_projection_from_projector_element(
|
|
element, i + 1, "preview", old["id"]
|
|
)
|
|
if projection_id > 0:
|
|
new["preview_projection_ids"].append(projection_id)
|
|
|
|
flat_history = [
|
|
item for sublist in old["elements_history"] for item in sublist
|
|
]
|
|
for i, element in enumerate(flat_history):
|
|
projection_id = self.create_projection_from_projector_element(
|
|
element, i + 1, "history", old["id"]
|
|
)
|
|
if projection_id > 0:
|
|
new["history_projection_ids"].append(projection_id)
|
|
|
|
if old["reference_projector_id"] == old["id"]:
|
|
self.meeting["reference_projector_id"] = old["id"]
|
|
new["used_as_reference_projector_meeting_id"] = 1
|
|
else:
|
|
new["used_as_reference_projector_meeting_id"] = None
|
|
|
|
new[
|
|
"used_as_default_$_in_meeting_id"
|
|
] = [] # will be filled when migrating the meeting
|
|
|
|
new["meeting_id"] = 1
|
|
self.set_model("projector", new)
|
|
|
|
def create_projection_from_projector_element(
|
|
self, element, weight, type, projector_id
|
|
):
|
|
"""
|
|
type can be "current", "preview" or "history"
|
|
registers the newly created projection and returns its id or returns -1 in case something went wrong
|
|
"""
|
|
projection = {
|
|
"id": self.projection_id_counter,
|
|
"stable": element.get("stable", True),
|
|
"weight": weight,
|
|
"options": {},
|
|
"current_projector_id": None,
|
|
"preview_projector_id": None,
|
|
"history_projector_id": None,
|
|
"meeting_id": 1,
|
|
}
|
|
projection[f"{type}_projector_id"] = projector_id
|
|
for k, v in element.items():
|
|
if k not in ("id", "name", "stable"):
|
|
projection["options"][k] = v
|
|
|
|
collection = element["name"]
|
|
if collection in COLLECTION_MAPPING:
|
|
id = self.to_new_id(collection, element["id"])
|
|
collection = COLLECTION_MAPPING[collection]
|
|
projection["content_object_id"] = f"{collection}/{id}"
|
|
projection["type"] = None
|
|
elif collection == "agenda/item-list":
|
|
collection = "meeting"
|
|
id = 1
|
|
projection["content_object_id"] = "meeting/1"
|
|
projection["type"] = "agenda_item_list"
|
|
elif collection in (
|
|
"agenda/current-list-of-speakers",
|
|
"agenda/current-list-of-speakers-overlay",
|
|
):
|
|
collection = "meeting"
|
|
id = 1
|
|
projection["content_object_id"] = "meeting/1"
|
|
projection["type"] = "current_list_of_speakers"
|
|
elif collection == "agenda/current-speaker-chyron":
|
|
collection = "meeting"
|
|
id = 1
|
|
projection["content_object_id"] = "meeting/1"
|
|
projection["type"] = "current_speaker_chyron"
|
|
elif collection == "core/clock":
|
|
# somehow the clock got into the preview/history, just ignore
|
|
return -1
|
|
else:
|
|
raise OS4ExporterException(f"Unknown slide {collection}")
|
|
|
|
if not self.exists_model(collection, id):
|
|
return -1
|
|
content_object = self.get_model(collection, id)
|
|
if collection != "user":
|
|
content_object["projection_ids"].append(projection["id"])
|
|
else:
|
|
if not content_object["projection_$_ids"]:
|
|
content_object["projection_$_ids"] = ["1"]
|
|
content_object["projection_$1_ids"] = []
|
|
content_object["projection_$1_ids"].append(projection["id"])
|
|
|
|
self.projection_id_counter += 1
|
|
self.set_model("projection", projection)
|
|
return projection["id"]
|
|
|
|
def migrate_meeting(self):
|
|
configs = {
|
|
config["key"]: config["value"]
|
|
for config in self.get_collection("core/config")
|
|
}
|
|
|
|
self.meeting["welcome_title"] = configs["general_event_welcome_title"]
|
|
self.meeting["welcome_text"] = configs["general_event_welcome_text"]
|
|
|
|
self.meeting["name"] = configs["general_event_name"]
|
|
self.meeting["description"] = configs["general_event_description"]
|
|
self.meeting["location"] = configs["general_event_location"]
|
|
self.meeting[
|
|
"start_time"
|
|
] = 0 # Since it is a freehand field in OS3, it cannot be parsed
|
|
self.meeting["end_time"] = 0
|
|
|
|
self.meeting["jitsi_domain"] = getattr(settings, "JITSI_DOMAIN", None)
|
|
self.meeting["jitsi_room_name"] = getattr(settings, "JITSI_ROOM_NAME", None)
|
|
self.meeting["jitsi_room_password"] = getattr(
|
|
settings, "JITSI_ROOM_PASSWORD", None
|
|
)
|
|
self.meeting["enable_chat"] = getattr(settings, "ENABLE_CHAT", False)
|
|
self.meeting["imported_at"] = None
|
|
|
|
self.meeting["url_name"] = None
|
|
self.meeting["template_for_committee_id"] = None
|
|
self.meeting["enable_anonymous"] = configs["general_system_enable_anonymous"]
|
|
self.meeting["custom_translations"] = configs["translations"]
|
|
|
|
self.meeting["conference_show"] = configs["general_system_conference_show"]
|
|
self.meeting["conference_auto_connect"] = configs[
|
|
"general_system_conference_auto_connect"
|
|
]
|
|
self.meeting["conference_los_restriction"] = configs[
|
|
"general_system_conference_los_restriction"
|
|
]
|
|
self.meeting["conference_stream_url"] = configs["general_system_stream_url"]
|
|
self.meeting["conference_stream_poster_url"] = configs[
|
|
"general_system_stream_poster"
|
|
]
|
|
self.meeting["conference_open_microphone"] = configs[
|
|
"general_system_conference_open_microphone"
|
|
]
|
|
self.meeting["conference_open_video"] = configs[
|
|
"general_system_conference_open_video"
|
|
]
|
|
self.meeting["conference_auto_connect_next_speakers"] = configs[
|
|
"general_system_conference_auto_connect_next_speakers"
|
|
]
|
|
self.meeting["conference_enable_helpdesk"] = configs[
|
|
"general_system_conference_enable_helpdesk"
|
|
]
|
|
|
|
self.meeting["applause_enable"] = configs["general_system_applause_enable"]
|
|
self.meeting["applause_type"] = configs["general_system_applause_type"]
|
|
self.meeting["applause_show_level"] = configs[
|
|
"general_system_applause_show_level"
|
|
]
|
|
self.meeting["applause_min_amount"] = configs[
|
|
"general_system_applause_min_amount"
|
|
]
|
|
self.meeting["applause_max_amount"] = configs[
|
|
"general_system_applause_max_amount"
|
|
]
|
|
self.meeting["applause_particle_image_url"] = configs[
|
|
"general_system_applause_particle_image"
|
|
]
|
|
self.meeting["applause_timeout"] = configs[
|
|
"general_system_stream_applause_timeout"
|
|
]
|
|
self.meeting["applause_timeout"] = configs[
|
|
"general_system_stream_applause_timeout"
|
|
]
|
|
|
|
self.meeting["projector_countdown_default_time"] = configs[
|
|
"projector_default_countdown"
|
|
]
|
|
self.meeting["projector_countdown_warning_time"] = configs[
|
|
"agenda_countdown_warning_time"
|
|
]
|
|
|
|
self.meeting["export_csv_encoding"] = configs["general_csv_encoding"]
|
|
self.meeting["export_csv_separator"] = configs["general_csv_separator"]
|
|
self.meeting["export_pdf_pagenumber_alignment"] = configs[
|
|
"general_export_pdf_pagenumber_alignment"
|
|
]
|
|
self.meeting["export_pdf_fontsize"] = int(
|
|
configs["general_export_pdf_fontsize"]
|
|
)
|
|
self.meeting["export_pdf_pagesize"] = configs["general_export_pdf_pagesize"]
|
|
|
|
self.meeting["agenda_show_subtitles"] = configs["agenda_show_subtitle"]
|
|
self.meeting["agenda_enable_numbering"] = configs["agenda_enable_numbering"]
|
|
prefix = configs["agenda_number_prefix"]
|
|
self.meeting["agenda_number_prefix"] = (
|
|
prefix if len(prefix) <= 20 else prefix[0:20]
|
|
)
|
|
self.meeting["agenda_numeral_system"] = configs["agenda_numeral_system"]
|
|
self.meeting["agenda_item_creation"] = configs["agenda_item_creation"]
|
|
self.meeting["agenda_new_items_default_visibility"] = {
|
|
"1": "common",
|
|
"2": "internal",
|
|
"3": "hidden",
|
|
}[configs["agenda_new_items_default_visibility"]]
|
|
self.meeting["agenda_show_internal_items_on_projector"] = not configs[
|
|
"agenda_hide_internal_items_on_projector"
|
|
]
|
|
|
|
self.meeting["list_of_speakers_amount_last_on_projector"] = configs[
|
|
"agenda_show_last_speakers"
|
|
]
|
|
self.meeting["list_of_speakers_amount_next_on_projector"] = configs[
|
|
"agenda_show_next_speakers"
|
|
]
|
|
self.meeting["list_of_speakers_couple_countdown"] = configs[
|
|
"agenda_couple_countdown_and_speakers"
|
|
]
|
|
self.meeting["list_of_speakers_show_amount_of_speakers_on_slide"] = not configs[
|
|
"agenda_hide_amount_of_speakers"
|
|
]
|
|
self.meeting["list_of_speakers_present_users_only"] = configs[
|
|
"agenda_present_speakers_only"
|
|
]
|
|
self.meeting["list_of_speakers_show_first_contribution"] = configs[
|
|
"agenda_show_first_contribution"
|
|
]
|
|
self.meeting["list_of_speakers_enable_point_of_order_speakers"] = configs[
|
|
"agenda_enable_point_of_order_speakers"
|
|
]
|
|
self.meeting["list_of_speakers_enable_pro_contra_speech"] = configs[
|
|
"agenda_list_of_speakers_enable_pro_contra_speech"
|
|
]
|
|
self.meeting["list_of_speakers_can_set_contribution_self"] = configs[
|
|
"agenda_list_of_speakers_can_set_mark_self"
|
|
]
|
|
self.meeting["list_of_speakers_speaker_note_for_everyone"] = configs[
|
|
"agenda_list_of_speakers_speaker_note_for_everyone"
|
|
]
|
|
self.meeting["list_of_speakers_initially_closed"] = configs[
|
|
"agenda_list_of_speakers_initially_closed"
|
|
]
|
|
|
|
workflow_id = int(configs["motions_workflow"])
|
|
workflow = self.get_model("motion_workflow", workflow_id)
|
|
workflow["default_workflow_meeting_id"] = 1
|
|
self.meeting["motions_default_workflow_id"] = workflow_id
|
|
|
|
workflow_id = int(configs["motions_amendments_workflow"])
|
|
workflow = self.get_model("motion_workflow", workflow_id)
|
|
workflow["default_amendment_workflow_meeting_id"] = 1
|
|
self.meeting["motions_default_amendment_workflow_id"] = workflow_id
|
|
|
|
workflow_id = int(configs["motions_statute_amendments_workflow"])
|
|
workflow = self.get_model("motion_workflow", workflow_id)
|
|
workflow["default_statute_amendment_workflow_meeting_id"] = 1
|
|
self.meeting["motions_default_statute_amendment_workflow_id"] = workflow_id
|
|
|
|
self.meeting["motions_preamble"] = configs["motions_preamble"]
|
|
self.meeting["motions_default_line_numbering"] = configs[
|
|
"motions_default_line_numbering"
|
|
]
|
|
self.meeting["motions_line_length"] = configs["motions_line_length"]
|
|
self.meeting["motions_reason_required"] = configs["motions_reason_required"]
|
|
self.meeting["motions_enable_text_on_projector"] = not configs[
|
|
"motions_disable_text_on_projector"
|
|
]
|
|
self.meeting["motions_enable_reason_on_projector"] = not configs[
|
|
"motions_disable_reason_on_projector"
|
|
]
|
|
self.meeting["motions_enable_sidebox_on_projector"] = not configs[
|
|
"motions_disable_sidebox_on_projector"
|
|
]
|
|
self.meeting["motions_enable_recommendation_on_projector"] = not configs[
|
|
"motions_disable_recommendation_on_projector"
|
|
]
|
|
self.meeting["motions_show_referring_motions"] = not configs[
|
|
"motions_hide_referring_motions"
|
|
]
|
|
self.meeting["motions_show_sequential_number"] = configs[
|
|
"motions_show_sequential_numbers"
|
|
]
|
|
self.meeting["motions_recommendations_by"] = configs[
|
|
"motions_recommendations_by"
|
|
]
|
|
self.meeting["motions_statute_recommendations_by"] = configs[
|
|
"motions_statute_recommendations_by"
|
|
]
|
|
self.meeting["motions_recommendation_text_mode"] = configs[
|
|
"motions_recommendation_text_mode"
|
|
]
|
|
self.meeting["motions_default_sorting"] = {
|
|
"identifier": "number",
|
|
"weight": "weight",
|
|
}[configs["motions_motions_sorting"]]
|
|
self.meeting["motions_number_type"] = configs["motions_identifier"]
|
|
self.meeting["motions_number_min_digits"] = configs[
|
|
"motions_identifier_min_digits"
|
|
]
|
|
self.meeting["motions_number_with_blank"] = configs[
|
|
"motions_identifier_with_blank"
|
|
]
|
|
self.meeting["motions_statutes_enabled"] = configs["motions_statutes_enabled"]
|
|
self.meeting["motions_amendments_enabled"] = configs[
|
|
"motions_amendments_enabled"
|
|
]
|
|
self.meeting["motions_amendments_in_main_list"] = configs[
|
|
"motions_amendments_main_table"
|
|
]
|
|
self.meeting["motions_amendments_of_amendments"] = configs[
|
|
"motions_amendments_of_amendments"
|
|
]
|
|
self.meeting["motions_amendments_prefix"] = configs["motions_amendments_prefix"]
|
|
self.meeting["motions_amendments_text_mode"] = configs[
|
|
"motions_amendments_text_mode"
|
|
]
|
|
self.meeting["motions_amendments_multiple_paragraphs"] = configs[
|
|
"motions_amendments_multiple_paragraphs"
|
|
]
|
|
self.meeting["motions_supporters_min_amount"] = configs[
|
|
"motions_min_supporters"
|
|
]
|
|
self.meeting["motions_export_title"] = configs["motions_export_title"]
|
|
self.meeting["motions_export_preamble"] = configs["motions_export_preamble"]
|
|
self.meeting["motions_export_submitter_recommendation"] = configs[
|
|
"motions_export_submitter_recommendation"
|
|
]
|
|
self.meeting["motions_export_follow_recommendation"] = configs[
|
|
"motions_export_follow_recommendation"
|
|
]
|
|
|
|
self.meeting["motion_poll_ballot_paper_selection"] = configs[
|
|
"motions_pdf_ballot_papers_selection"
|
|
]
|
|
self.meeting["motion_poll_ballot_paper_number"] = configs[
|
|
"motions_pdf_ballot_papers_number"
|
|
]
|
|
self.meeting["motion_poll_default_type"] = configs["motion_poll_default_type"]
|
|
self.meeting["motion_poll_default_100_percent_base"] = configs[
|
|
"motion_poll_default_100_percent_base"
|
|
]
|
|
self.meeting["motion_poll_default_backend"] = "fast"
|
|
|
|
group_ids = configs["motion_poll_default_groups"]
|
|
for group_id in group_ids:
|
|
group = self.get_model("group", group_id)
|
|
group["used_as_motion_poll_default_id"] = 1
|
|
self.meeting["motion_poll_default_group_ids"] = group_ids
|
|
|
|
self.meeting["users_sort_by"] = configs["users_sort_by"]
|
|
self.meeting["users_enable_presence_view"] = configs[
|
|
"users_enable_presence_view"
|
|
]
|
|
self.meeting["users_enable_vote_weight"] = configs["users_activate_vote_weight"]
|
|
self.meeting["users_allow_self_set_present"] = configs[
|
|
"users_allow_self_set_present"
|
|
]
|
|
self.meeting["users_pdf_welcometitle"] = configs["users_pdf_welcometitle"]
|
|
self.meeting["users_pdf_welcometext"] = configs["users_pdf_welcometext"]
|
|
self.meeting["users_pdf_url"] = configs["users_pdf_url"]
|
|
self.meeting["users_pdf_wlan_ssid"] = configs["users_pdf_wlan_ssid"]
|
|
self.meeting["users_pdf_wlan_password"] = configs["users_pdf_wlan_password"]
|
|
self.meeting["users_pdf_wlan_encryption"] = configs["users_pdf_wlan_encryption"]
|
|
self.meeting["users_email_sender"] = configs["users_email_sender"]
|
|
self.meeting["users_email_replyto"] = configs["users_email_replyto"]
|
|
self.meeting["users_email_subject"] = configs["users_email_subject"]
|
|
self.meeting["users_email_body"] = configs["users_email_body"]
|
|
|
|
self.meeting["assignments_export_title"] = configs["assignments_pdf_title"]
|
|
self.meeting["assignments_export_preamble"] = configs[
|
|
"assignments_pdf_preamble"
|
|
]
|
|
|
|
self.meeting["assignment_poll_ballot_paper_selection"] = configs[
|
|
"assignments_pdf_ballot_papers_selection"
|
|
]
|
|
self.meeting["assignment_poll_ballot_paper_number"] = configs[
|
|
"assignments_pdf_ballot_papers_number"
|
|
]
|
|
self.meeting["assignment_poll_add_candidates_to_list_of_speakers"] = configs[
|
|
"assignment_poll_add_candidates_to_list_of_speakers"
|
|
]
|
|
self.meeting["assignment_poll_sort_poll_result_by_votes"] = configs[
|
|
"assignment_poll_sort_poll_result_by_votes"
|
|
]
|
|
self.meeting["assignment_poll_default_type"] = configs[
|
|
"assignment_poll_default_type"
|
|
]
|
|
self.meeting["assignment_poll_default_method"] = configs[
|
|
"assignment_poll_method"
|
|
]
|
|
self.meeting["assignment_poll_default_100_percent_base"] = configs[
|
|
"assignment_poll_default_100_percent_base"
|
|
]
|
|
self.meeting["assignment_poll_default_backend"] = "fast"
|
|
|
|
group_ids = configs["assignment_poll_default_groups"]
|
|
for group_id in group_ids:
|
|
group = self.get_model("group", group_id)
|
|
group["used_as_assignment_poll_default_id"] = 1
|
|
self.meeting["assignment_poll_default_group_ids"] = group_ids
|
|
|
|
self.meeting["poll_ballot_paper_selection"] = "CUSTOM_NUMBER"
|
|
self.meeting["poll_ballot_paper_number"] = 8
|
|
self.meeting["poll_sort_poll_result_by_votes"] = True
|
|
self.meeting["poll_default_type"] = "analog"
|
|
self.meeting["poll_default_method"] = "Y"
|
|
self.meeting["poll_default_100_percent_base"] = "YNA"
|
|
self.meeting["poll_default_backend"] = "fast"
|
|
self.meeting["poll_default_group_ids"] = []
|
|
self.meeting["poll_couple_countdown"] = True
|
|
|
|
for collection in (
|
|
"projector",
|
|
"projector_message",
|
|
"projector_countdown",
|
|
"tag",
|
|
"agenda_item",
|
|
"list_of_speakers",
|
|
"speaker",
|
|
"topic",
|
|
"group",
|
|
"mediafile",
|
|
"motion",
|
|
"motion_comment_section",
|
|
"motion_category",
|
|
"motion_block",
|
|
"motion_workflow",
|
|
"motion_statute_paragraph",
|
|
"motion_comment",
|
|
"motion_submitter",
|
|
"motion_change_recommendation",
|
|
"motion_state",
|
|
"poll",
|
|
"option",
|
|
"vote",
|
|
"assignment",
|
|
"assignment_candidate",
|
|
"personal_note",
|
|
"chat_group",
|
|
"chat_message",
|
|
):
|
|
self.meeting[f"{collection}_ids"] = [
|
|
x["id"] for x in self.iter_collection(collection)
|
|
]
|
|
|
|
self.meeting["all_projection_ids"] = [
|
|
x["id"] for x in self.iter_collection("projection")
|
|
]
|
|
# projection_ids was set when creating self.meeting
|
|
|
|
self.migrate_logos_and_fonts(configs, "logo")
|
|
self.migrate_logos_and_fonts(configs, "font")
|
|
|
|
self.meeting["committee_id"] = None
|
|
self.meeting["default_meeting_for_committee_id"] = None
|
|
self.meeting["is_active_in_organization_id"] = None
|
|
self.meeting["organization_tag_ids"] = []
|
|
self.meeting["present_user_ids"] = [
|
|
x["id"]
|
|
for x in self.iter_collection("user")
|
|
if 1 in x["is_present_in_meeting_ids"]
|
|
]
|
|
self.meeting["user_ids"] = [x["id"] for x in self.iter_collection("user")]
|
|
# reference_projector_id is set by the projector migration
|
|
# list_of_speakers_countdown_id and poll_countdown_id are set by the countdown migration
|
|
|
|
self.meeting["default_projector_$_id"] = []
|
|
for pd in self.get_collection("core/projection-default"):
|
|
name = PROJECTION_DEFAULT_NAME_MAPPING[pd["name"]]
|
|
projector = self.get_model("projector", pd["projector_id"])
|
|
projector["used_as_default_$_in_meeting_id"].append(name)
|
|
projector[f"used_as_default_${name}_in_meeting_id"] = 1
|
|
self.meeting["default_projector_$_id"].append(name)
|
|
self.meeting[f"default_projector_${name}_id"] = pd["projector_id"]
|
|
|
|
# Add "poll"
|
|
projector_id = self.meeting["projector_ids"][0] # get an arbitrary projector id
|
|
projector = self.get_model("projector", projector_id)
|
|
projector["used_as_default_$_in_meeting_id"].append("poll")
|
|
projector["used_as_default_$poll_in_meeting_id"] = 1
|
|
self.meeting["default_projector_$_id"].append("poll")
|
|
self.meeting["default_projector_$poll_id"] = projector_id
|
|
|
|
# default_group_id and admin_group_id are set by the group migration
|
|
|
|
def migrate_logos_and_fonts(self, configs, type):
|
|
self.meeting[f"{type}_$_id"] = []
|
|
for place in configs[f"{type}s_available"]:
|
|
path = configs[place].get("path", "")
|
|
if not path:
|
|
continue
|
|
# find mediafile
|
|
mediafile_id = None
|
|
for m in self.get_collection("mediafiles/mediafile"):
|
|
m_path = m["media_url_prefix"] + m["path"]
|
|
if m_path == path:
|
|
mediafile_id = m["id"]
|
|
break
|
|
if not mediafile_id:
|
|
continue
|
|
|
|
replacement = place.split("_", 2)[1]
|
|
mediafile = self.get_model("mediafile", mediafile_id)
|
|
mediafile[f"used_as_{type}_$_in_meeting_id"].append(replacement)
|
|
mediafile[f"used_as_{type}_${replacement}_in_meeting_id"] = 1
|
|
self.meeting[f"{type}_$_id"].append(replacement)
|
|
self.meeting[f"{type}_${replacement}_id"] = mediafile_id
|