|
| 1 | +import logging |
| 2 | + |
| 3 | +from django.core.management.base import BaseCommand, CommandError |
| 4 | +from django.db import transaction |
| 5 | + |
| 6 | +from projects.models import Project |
| 7 | + |
| 8 | +logger = logging.getLogger(__name__) |
| 9 | + |
| 10 | +XL_ATTRIBUTES = { |
| 11 | + 'jarjestetaan_periaatteet_esillaolo_1': True, |
| 12 | + 'jarjestetaan_periaatteet_esillaolo_2': False, |
| 13 | + 'jarjestetaan_periaatteet_esillaolo_3': False, |
| 14 | + 'periaatteet_lautakuntaan_1': True, |
| 15 | + 'periaatteet_lautakuntaan_2': False, |
| 16 | + 'periaatteet_lautakuntaan_3': False, |
| 17 | + 'periaatteet_lautakuntaan_4': False, |
| 18 | + 'jarjestetaan_luonnos_esillaolo_1': True, |
| 19 | + 'jarjestetaan_luonnos_esillaolo_2': False, |
| 20 | + 'jarjestetaan_luonnos_esillaolo_3': False, |
| 21 | + 'kaavaluonnos_lautakuntaan_1': True, |
| 22 | + 'kaavaluonnos_lautakuntaan_2': False, |
| 23 | + 'kaavaluonnos_lautakuntaan_3': False, |
| 24 | + 'kaavaluonnos_lautakuntaan_4': False, |
| 25 | + 'kaavaehdotus_lautakuntaan_1': True, |
| 26 | + 'kaavaehdotus_lautakuntaan_2': False, |
| 27 | + 'kaavaehdotus_lautakuntaan_3': False, |
| 28 | + 'kaavaehdotus_lautakuntaan_4': False, |
| 29 | +} |
| 30 | + |
| 31 | +L_ATTRIBUTES = { |
| 32 | + 'kaavaehdotus_lautakuntaan_1': True, |
| 33 | + 'kaavaehdotus_lautakuntaan_2': False, |
| 34 | + 'kaavaehdotus_lautakuntaan_3': False, |
| 35 | + 'kaavaehdotus_lautakuntaan_4': False, |
| 36 | +} |
| 37 | + |
| 38 | +COMMON_ATTRIBUTES = { |
| 39 | + 'jarjestetaan_oas_esillaolo_1': True, |
| 40 | + 'jarjestetaan_oas_esillaolo_2': False, |
| 41 | + 'jarjestetaan_oas_esillaolo_3': False, |
| 42 | + 'kaavaehdotus_nahtaville_1': True, |
| 43 | + 'kaavaehdotus_uudelleen_nahtaville_2': False, |
| 44 | + 'kaavaehdotus_uudelleen_nahtaville_3': False, |
| 45 | + 'kaavaehdotus_uudelleen_nahtaville_4': False, |
| 46 | + 'tarkistettu_ehdotus_lautakuntaan_1': True, |
| 47 | + 'tarkistettu_ehdotus_lautakuntaan_2': False, |
| 48 | + 'tarkistettu_ehdotus_lautakuntaan_3': False, |
| 49 | + 'tarkistettu_ehdotus_lautakuntaan_4': False |
| 50 | +} |
| 51 | + |
| 52 | +class Command(BaseCommand): |
| 53 | + help = "Adds default values for missing visibility boolean attributes in existing projects" |
| 54 | + |
| 55 | + def add_arguments(self, parser): |
| 56 | + parser.add_argument("--id", nargs="?", type=int) |
| 57 | + parser.add_argument( |
| 58 | + "--dry-run", |
| 59 | + action="store_true", |
| 60 | + help="Show planned changes without saving them", |
| 61 | + ) |
| 62 | + parser.add_argument( |
| 63 | + "--execute", |
| 64 | + action="store_true", |
| 65 | + help="Apply the changes after confirmation", |
| 66 | + ) |
| 67 | + |
| 68 | + def handle(self, *args, **options): |
| 69 | + project_id = options.get("id") |
| 70 | + dry_run = options.get("dry_run", False) |
| 71 | + execute = options.get("execute", False) |
| 72 | + |
| 73 | + if dry_run == execute: |
| 74 | + raise CommandError("Specify exactly one of --dry-run or --execute") |
| 75 | + |
| 76 | + projects = self._get_projects(project_id) |
| 77 | + project_changes, change_log = self._collect_changes(projects) |
| 78 | + |
| 79 | + self._print_summary(project_changes, dry_run=dry_run) |
| 80 | + |
| 81 | + if not project_changes: |
| 82 | + logger.info("No missing visibility boolean attributes found") |
| 83 | + return |
| 84 | + |
| 85 | + if dry_run: |
| 86 | + self._maybe_write_log(change_log) |
| 87 | + return |
| 88 | + |
| 89 | + if (input("Apply changes to database? (y/n): ") or "").lower() != "y": |
| 90 | + raise CommandError("Aborting without saving changes to database") |
| 91 | + |
| 92 | + with transaction.atomic(): |
| 93 | + for project, changes in project_changes: |
| 94 | + for attribute, default_value in changes: |
| 95 | + project.attribute_data[attribute] = default_value |
| 96 | + project.save() |
| 97 | + |
| 98 | + logger.info( |
| 99 | + "Finished updating missing visibility boolean attributes for %s projects", |
| 100 | + len(project_changes), |
| 101 | + ) |
| 102 | + self._maybe_write_log(change_log) |
| 103 | + |
| 104 | + def _get_projects(self, project_id=None): |
| 105 | + if project_id is None: |
| 106 | + return Project.objects.filter(archived=False).select_related("subtype") |
| 107 | + |
| 108 | + try: |
| 109 | + project = Project.objects.select_related("subtype").get(pk=project_id) |
| 110 | + except Project.DoesNotExist as error: |
| 111 | + raise CommandError(f"Project with id={project_id} does not exist") from error |
| 112 | + |
| 113 | + return [project] |
| 114 | + |
| 115 | + def _collect_changes(self, projects): |
| 116 | + project_changes = [] |
| 117 | + change_log = [] |
| 118 | + |
| 119 | + for project in projects: |
| 120 | + changes = self._collect_project_changes(project) |
| 121 | + if not changes: |
| 122 | + continue |
| 123 | + |
| 124 | + project_changes.append((project, changes)) |
| 125 | + for attribute, default_value in changes: |
| 126 | + logger.info( |
| 127 | + "Set %s to %s for project %s", |
| 128 | + attribute, |
| 129 | + default_value, |
| 130 | + project.name, |
| 131 | + ) |
| 132 | + change_log.append( |
| 133 | + f"Set {attribute} to {default_value} for project {project.name}" |
| 134 | + ) |
| 135 | + |
| 136 | + logger.info("Would update attribute_data for project %s %s", project.id, project.name) |
| 137 | + change_log.append( |
| 138 | + f"Updated attribute_data for project {project.id} {project.name}\n---" |
| 139 | + ) |
| 140 | + |
| 141 | + return project_changes, change_log |
| 142 | + |
| 143 | + def _collect_project_changes(self, project): |
| 144 | + changes = [] |
| 145 | + current_values = project.attribute_data |
| 146 | + |
| 147 | + for attribute, default_value in COMMON_ATTRIBUTES.items(): |
| 148 | + if current_values.get(attribute) is None: |
| 149 | + changes.append((attribute, default_value)) |
| 150 | + |
| 151 | + subtype_attributes = {} |
| 152 | + if project.subtype.name == "XL": |
| 153 | + subtype_attributes = XL_ATTRIBUTES |
| 154 | + elif project.subtype.name == "L": |
| 155 | + subtype_attributes = L_ATTRIBUTES |
| 156 | + |
| 157 | + for attribute, default_value in subtype_attributes.items(): |
| 158 | + if (not project.create_draft and "luonnos" in attribute) or ( |
| 159 | + not project.create_principles and "periaatteet" in attribute |
| 160 | + ): |
| 161 | + continue |
| 162 | + if current_values.get(attribute) is None: |
| 163 | + changes.append((attribute, default_value)) |
| 164 | + |
| 165 | + return changes |
| 166 | + |
| 167 | + def _print_summary(self, project_changes, dry_run=False): |
| 168 | + mode = "DRY RUN" if dry_run else "EXECUTE" |
| 169 | + self.stdout.write(self.style.NOTICE( |
| 170 | + f"Running add_missing_vis_bools in {mode} mode" |
| 171 | + )) |
| 172 | + |
| 173 | + if not project_changes: |
| 174 | + self.stdout.write(self.style.SUCCESS("No projects need updates")) |
| 175 | + return |
| 176 | + |
| 177 | + self.stdout.write( |
| 178 | + self.style.WARNING(f"Projects to update: {len(project_changes)}") |
| 179 | + ) |
| 180 | + for project, changes in project_changes: |
| 181 | + self.stdout.write(f"- {project.id} {project.name}: {len(changes)} changes") |
| 182 | + for attribute, default_value in changes: |
| 183 | + self.stdout.write(f" {attribute} -> {default_value}") |
| 184 | + |
| 185 | + def _maybe_write_log(self, change_log): |
| 186 | + if (input("Write changes to file? (y/n): ") or "").lower() != "y": |
| 187 | + return |
| 188 | + |
| 189 | + try: |
| 190 | + with open("missing_vis_bools_changes.txt", "w") as file_obj: |
| 191 | + file_obj.write("\n".join(change_log)) |
| 192 | + logger.info("Changes written to missing_vis_bools_changes.txt") |
| 193 | + except Exception as error: |
| 194 | + logger.error("Failed to write changes to file: %s", error) |
0 commit comments