diff --git a/client/src/app/site/common/components/legal-notice/legal-notice.component.ts b/client/src/app/site/common/components/legal-notice/legal-notice.component.ts index cb295c6a2..0d7d2ba89 100644 --- a/client/src/app/site/common/components/legal-notice/legal-notice.component.ts +++ b/client/src/app/site/common/components/legal-notice/legal-notice.component.ts @@ -3,6 +3,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { environment } from 'environments/environment.prod'; import { DataStoreService } from 'app/core/core-services/data-store.service'; import { OpenSlidesService } from 'app/core/core-services/openslides.service'; diff --git a/client/src/app/site/config/components/config-overview/config-overview.component.html b/client/src/app/site/config/components/config-overview/config-overview.component.html index 37f8c2e6f..13ca110de 100644 --- a/client/src/app/site/config/components/config-overview/config-overview.component.html +++ b/client/src/app/site/config/components/config-overview/config-overview.component.html @@ -47,4 +47,8 @@ undo {{ 'Reset to factory defaults' | translate }} + diff --git a/client/src/app/site/config/components/config-overview/config-overview.component.ts b/client/src/app/site/config/components/config-overview/config-overview.component.ts index 0d7201786..47f8a527e 100644 --- a/client/src/app/site/config/components/config-overview/config-overview.component.ts +++ b/client/src/app/site/config/components/config-overview/config-overview.component.ts @@ -2,9 +2,13 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { environment } from 'environments/environment.prod'; import { BaseComponent } from 'app/base.component'; +import { HttpService } from 'app/core/core-services/http.service'; +import { OperatorService } from 'app/core/core-services/operator.service'; import { ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service'; +import { FileExportService } from 'app/core/ui-services/file-export.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; /** @@ -22,11 +26,18 @@ export class ConfigOverviewComponent extends BaseComponent implements OnInit { protected titleService: Title, protected translate: TranslateService, public repo: ConfigRepositoryService, - private promptDialog: PromptService + private promptDialog: PromptService, + private http: HttpService, + private exporter: FileExportService, + private operator: OperatorService ) { super(titleService, translate); } + public isSuperAdmin(): boolean { + return this.operator.isSuperAdmin; + } + /** * Sets the title, inits the table and calls the repo */ @@ -49,4 +60,10 @@ export class ConfigOverviewComponent extends BaseComponent implements OnInit { await this.repo.resetGroups(this.groups); } } + + public async exportToOS4(): Promise { + const data = await this.http.get(environment.urlPrefix + '/core/os4-export/'); + const json = JSON.stringify(data, null, 2); + this.exporter.saveFile(json, 'export.json', 'application/json'); + } } diff --git a/server/openslides/core/export.py b/server/openslides/core/export.py new file mode 100644 index 000000000..35d2c315a --- /dev/null +++ b/server/openslides/core/export.py @@ -0,0 +1,1740 @@ +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 use_mediafile_database +from openslides.motions.models import Motion +from openslides.users.views import demo_mode_users, is_demo_mode +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() + + # Note: When returning self.all_data one has access to the original data to compare it to the export. + # return {"all": self.all_data, "export": self.to_list_format()} + return self.to_list_format() + + 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 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 = {} + 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.poll_id_counter = ( + max_or_zero([x["id"] for x in self.get_collection("motions/motion-poll")]) + + 1 + ) + self.option_id_counter = ( + max_or_zero([x["id"] for x in self.get_collection("motions/motion-option")]) + + 1 + ) + self.vote_id_counter = ( + max_or_zero([x["id"] for x in self.get_collection("motions/motion-vote")]) + + 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["option_ids"] = [] + 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", + "majority_method", + "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"] = "long" + 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.poll_id_counter + self.poll_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 + 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( + "SELECT data FROM mediafile_data 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\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["forwarding_tree_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["restrictions"] = [ + { + "motions.can_see_internal": "motion.can_see_internal", + "motions.can_manage_metadata": "motion.can_manage_metadata", + "motions.can_manage": "motion.can_manage", + "is_submitter": "is_submitter", + }[restriction] + for restriction in old["restriction"] + ] + 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) + new = { + "id": id_counter, + "user_id": old["user_id"], + "content_object_id": f"motion/{motion_id}", + "note": note["note"], + "star": note["star"], + "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", + ) + + 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] + + 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"] + ) + 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"] + ) + new["preview_projection_ids"].append(projection_id) + + flat_history = [ + item for sublist in old["elements_history"] for item in sublist + ] + for i, elements in enumerate(flat_history): + projection_id = self.create_projection_from_projector_element( + element, i + 1, "history", old["id"] + ) + 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" """ + 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" + else: + raise OS4ExporterException(f"Unknown slide {collection}") + + if collection != "user": + content_object = self.get_model(collection, id) + content_object["projection_ids"].append(projection["id"]) + else: + user = self.get_model(collection, id) + if not user["projection_$_ids"]: + user["projection_$_ids"] = ["1"] + user["projection_$1_ids"] = [] + user["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" + ] + + # TODO: missing setting in OS4 + # self.meeting["conference_enable_helpdesk"] = configs["general_system_conference_enable_helpdesk"] + + 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"] = 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_majority_method"] = configs[ + "motion_poll_default_majority_method" + ] + + 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_majority_method"] = configs[ + "assignment_poll_default_majority_method" + ] + + 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_majority_method"] = "simple" + 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", + ): + 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["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 diff --git a/server/openslides/core/urls.py b/server/openslides/core/urls.py index 564d1ae07..163ec75d2 100644 --- a/server/openslides/core/urls.py +++ b/server/openslides/core/urls.py @@ -13,4 +13,5 @@ urlpatterns = [ name="core_history_information", ), url(r"^history/data/$", views.HistoryDataView.as_view(), name="core_history_data"), + url(r"^os4-export/$", views.OS4ExportView.as_view(), name="core_os4_export"), ] diff --git a/server/openslides/core/views.py b/server/openslides/core/views.py index e80c7c01a..8b20767fb 100644 --- a/server/openslides/core/views.py +++ b/server/openslides/core/views.py @@ -40,6 +40,7 @@ from ..utils.rest_api import ( ) from .config import config from .exceptions import ConfigError, ConfigNotFound +from .export import OS4Exporter, OS4ExporterException from .models import ( ConfigStore, Countdown, @@ -720,3 +721,20 @@ class HistoryDataView(utils_views.APIView): collection: list(dataset[collection].values()) for collection in dataset.keys() } + + +class OS4ExportView(utils_views.APIView): + """ + Returns the server time as UNIX timestamp. + """ + + http_method_names = ["get"] + + def get_context_data(self, **context): + if not in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK]): + self.permission_denied(self.request) + + try: + return OS4Exporter().get_data() + except OS4ExporterException as e: + raise ValidationError({"detail": str(e)}) diff --git a/server/setup.cfg b/server/setup.cfg index f7d3e08e8..d552d33db 100644 --- a/server/setup.cfg +++ b/server/setup.cfg @@ -3,6 +3,7 @@ source = openslides omit = openslides/core/management/commands/*.py openslides/users/management/commands/*.py + openslides/core/export.py [coverage:html] directory = personal_data/tmp/htmlcov