Skip to content
Snippets Groups Projects
Commit 78b599bf authored by Tomáš Valenta's avatar Tomáš Valenta
Browse files

Merge branch 'test' into 'master'

Release

See merge request !1
parents 08e7370d dc36ac29
No related branches found
No related tags found
1 merge request!1Release
Pipeline #12462 passed
Showing
with 2212 additions and 49 deletions
......@@ -8,3 +8,4 @@ shared/static/shared/*.css
webpack-stats.json
.venv
media/*
git
#!/usr/bin/make -f
PYTHON = python
PYTHON = python3.10
VENV = .venv
PORT = 8013
SETTINGS = registry.settings.dev
SETTINGS = registry.settings.production
.PHONY: help venv install install-hooks hooks build run shell migrations migrate
......
......@@ -212,7 +212,7 @@ class ContractAdmin(
"types",
"summary",
"is_public",
"legal_state",
"is_valid",
"primary_contract",
]
},
......@@ -277,10 +277,7 @@ class ContractAdmin(
"publishing_rejection_comment",
)
if (
obj is not None
and request.user.has_perm("approve", obj)
):
if obj is not None and request.user.has_perm("approve", obj):
fieldsets.insert(
5,
("Schválení", {"fields": ["is_approved"]}),
......@@ -369,7 +366,7 @@ class ContractAdmin(
list_filter = (
"types",
"is_approved",
"legal_state",
"is_valid",
"is_public",
"paper_form_state",
"issues",
......
import io
import os
import string
from datetime import date, datetime
import yaml
from django.conf import settings
from django.core.management.base import BaseCommand
from git import Repo
from ...models import Contract, ContractFilingArea, ContractType
class Command(BaseCommand):
help = "Sync contract data from a remote Git repository."
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.normal_import_count = 0
self.partial_import_count = 0
self.already_imported_count = 0
self.issue_count = 0
self.fatal_error_count = 0
def add_arguments(self, parser):
parser.add_argument(
"repo_url",
type=str,
help="URL of the Git repository to clone",
)
parser.add_argument(
"branch",
type=str,
help="Branch to use",
)
parser.add_argument(
"--directory",
type=str,
help="Directory to store the cloned repository in",
)
parser.add_argument(
"--existing",
action="store_true",
help="Use the existing storage directory, as long as it exists.",
)
parser.add_argument(
"--purge",
action="store_true",
help="Purge all previous contracts before the import.",
)
def parse_index(
self,
contract_root: str,
open_file,
) -> dict:
split_contents = open_file.read().split("---")
if len(split_contents) < 2:
raise ValueError(f"{contract_root} index does not have valid metadata.")
# Use Jan 1, 1970 as the default, if any defined year is 0.
# Use Jan 1, 2100 as the default for infinitely valid contracts.
split_contents[1] = (
split_contents[1]
.replace("0000-00-00", "1970-01-01")
.replace("2222-22-22", "2100-01-01")
.replace("2222-00-00", "2100-01-01")
.replace("-32", "-31")
.replace("\t", " ")
)
yaml_source = split_contents[1]
try:
parsed_metadata = yaml.safe_load(io.StringIO(yaml_source))
except yaml.YAMLError as exc:
raise ValueError(
f"Failed to parse {contract_root} metadata: {exc}."
) from exc
if parsed_metadata is None:
raise ValueError(f"Got no metadata from {contract_root}.")
return parsed_metadata
def assign_contract_metadata(
self,
contract: Contract,
metadata: dict,
slug: str,
) -> None:
filing_area = None
types = []
is_already_imported = False
observed_issues_count = 0
for key, value in metadata.items():
key = key.strip()
if isinstance(value, str):
value = value.strip()
match key:
case "datum účinnosti":
if isinstance(value, date):
if date.year not in (1970, 2100): # Ignore placeholder years
contract.valid_start_date = value
elif value is not None:
observed_issues_count += 1
contract.notes += f"Špatně zadaný začátek platnosti: {value}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Contract {slug} has a broken valid start date: {value}."
)
)
case "datum ukončení":
if isinstance(value, date):
if date.year not in (1970, 2100): # Ignore placeholder years
contract.valid_end_date = value
elif value is not None and (
isinstance(value, str) and value.lower() != "na dobu neurčitou"
):
observed_issues_count += 1
contract.notes += f"Špatně zadaný konec platnosti: {value}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Contract {slug} has a broken valid end date: {value}."
)
)
case "title":
contract.name = value
if Contract.objects.filter(name=value).exists():
value += f"(DUPLIKÁT - {slug})"
if Contract.objects.filter(name=value).exists():
is_already_imported = True
if self.verbosity >= 1:
self.stdout.write(f"{slug} already exists, skipping.")
break
case "použité smluvní typy":
if isinstance(value, str):
value = string.capwords(value)
try:
type_instance = ContractType.objects.get(name=value)
except ContractType.DoesNotExist:
type_instance = ContractType(name=value)
types.append(type_instance)
continue
elif not isinstance(value, list):
observed_issues_count += 1
contract.notes += f"Špatně zadané typy: {value}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Contract {slug} is missing types - not a list: {value}."
)
)
continue
for type_name in value:
if not isinstance(type_name, str):
observed_issues_count += 1
contract.notes += f"Nezaevidovaný typ: {type_name}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Contract {slug} is missing types - list item is not a string: {value}."
)
)
continue
type_name = string.capwords(type_name.strip())
try:
type_instance = ContractType.objects.get(name=type_name)
except ContractType.DoesNotExist:
type_instance = ContractType(name=type_name)
types.append(type_instance)
case "předmět":
contract.summary = value
case "stav":
lower_value = value.lower()
if (
lower_value.startswith("splněn")
or lower_value.startswith("ukončen")
or lower_value.startswith("vypovězen")
or lower_value.startswith("odstoupen")
or lower_value in ("spněno",)
):
contract.is_valid = False
elif (
lower_value.startswith("platn")
or lower_value.startswith("řešen")
or lower_value in ("v plnění", "v řešení")
):
contract.is_valid = True
else:
observed_issues_count += 1
contract.notes += f"Neznámý stav: {value}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Contract {slug} has an invalid state: {value}."
)
)
case "náklady":
if isinstance(value, str):
formatted_value = value.replace(" ", "").replace(".00", ".0")
if formatted_value.isnumeric():
value = float(formatted_value)
if isinstance(value, (int, float)):
if value < 0:
observed_issues_count += 1
contract.notes += (
f"Původní, špatně zadané náklady: {value}\n"
)
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Contract {slug} has an invalid cost amount: {value}."
)
)
continue
if value != 0:
contract.cost_amount = int(value)
contract.cost_unit = contract.CostUnits.TOTAL
elif isinstance(value, str) and value.endswith("Kč/h"):
split_value = value.split("Kč/h")
if (
len(split_value) != 2
or not split_value[0].strip().isnumeric()
):
observed_issues_count += 1
contract.notes += f"Původní, neropoznané náklady: {value}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Could not parse cost for contract {slug}: {value}."
)
)
continue
contract.cost_amount = int(split_value[0].strip())
contract.cost_unit = contract.CostUnits.HOUR
elif value not in (None, "0"):
observed_issues_count += 1
contract.notes += f"Původní, neropoznané náklady: {value}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Could not parse cost for contract {slug}: {value}."
)
)
case "místo uložení":
if isinstance(value, str):
value = string.capwords(value) # Some normalization
try:
filing_area = ContractFilingArea.objects.get(name=value)
except ContractFilingArea.DoesNotExist:
if isinstance(value, str):
filing_area = ContractFilingArea(name=value)
else:
observed_issues_count += 1
contract.notes += f"Špatně zadaná spisovna: {value}\n"
if self.verbosity >= 2:
self.stderr.write(
self.style.NOTICE(
f"Contract {slug} has an invalid filing area: {value}."
)
)
continue
if not is_already_imported:
if observed_issues_count != 0:
self.partial_import_count += 1
self.issue_count += observed_issues_count
else:
self.normal_import_count += 1
if filing_area is not None:
filing_area.save()
for type_ in types:
type_.save()
# Save primary key first
contract.save()
contract.filing_area = filing_area
contract.types.set(types)
contract.save()
else:
self.already_imported_count += 1
def import_contract_from_files(
self, contract_root: str, files: list[str], valid_start_date: datetime
) -> None:
contract = Contract(notes="")
for file_ in files:
with open(
os.path.join(
contract_root,
file_,
),
"r",
) as open_file:
if file_ == "index.html":
metadata_failed = True
try:
metadata = self.parse_index(contract_root, open_file)
except ValueError as exc:
if self.verbosity >= 1:
self.stderr.write(
self.style.WARNING(
f"Could not parse {contract_root} metadata: {exc}"
)
)
self.fatal_error_count += 1
continue
self.assign_contract_metadata(
contract,
metadata,
os.path.basename(contract_root),
)
elif file_.endswith(".pdf"):
# TODO
continue
contract.save()
def import_all_contracts(self, git_dir) -> None:
year_root = os.path.join(
git_dir,
"smlouvy",
)
for year_directory in sorted(os.listdir(year_root)):
if int(year_directory) == 0:
continue # Out of range, TODO
month_root = os.path.join(
year_root,
year_directory,
)
for month_directory in sorted(os.listdir(month_root)):
day_root = os.path.join(
month_root,
month_directory,
)
for day_directory in sorted(os.listdir(day_root)):
contract_root = os.path.join(
git_dir,
"smlouvy",
year_directory,
month_directory,
day_directory,
)
for contract_directory in sorted(os.listdir(contract_root)):
this_contract_directory = os.path.join(
contract_root,
contract_directory,
)
if not os.path.isdir(this_contract_directory):
if self.verbosity >= 1:
self.stderr.write(
self.style.WARNING(
f"{this_contract_directory} is not a directory and thus invalid, skipping."
)
)
self.fatal_error_count += 1
continue
valid_start_date = datetime(
year=int(year_directory),
month=int(month_directory),
day=int(day_directory),
)
self.import_contract_from_files(
this_contract_directory,
os.listdir(this_contract_directory),
valid_start_date,
)
if self.verbosity >= 1:
self.stdout.write(
self.style.SUCCESS(
"\n"
f"Saved a total of {self.normal_import_count + self.partial_import_count} contracts.\n"
f" {self.partial_import_count} contained a total of {self.issue_count} issues.\n"
f" {self.already_imported_count} were already saved previously and skipped.\n"
f" {self.fatal_error_count} potential contracts were unparseable."
)
)
def handle(self, *args, **options) -> None:
self.verbosity = options["verbosity"]
git_dir = os.path.join(
os.getcwd(),
(options["directory"] if options["directory"] is not None else "git"),
)
if os.path.exists(git_dir):
if not options["existing"]:
if self.verbosity >= 1:
self.stderr.write(
self.style.ERROR(
f"Temporary git storage directory ({git_dir}) already exists.\n"
"As it could contain other data, it will not be removed.\n"
"Please remove it manually and try again or use the '--existing' "
"argument."
)
)
return
else:
if self.verbosity >= 2:
self.stdout.write("Using existing git storage directory.")
else:
if self.verbosity >= 2:
self.stdout.write("Cloning repository.")
Repo.clone_from(
options["repo_url"],
git_dir,
branch=options["branch"],
)
if self.verbosity >= 1:
self.stdout.write(self.style.SUCCESS("Finished cloning repository."))
if options["purge"]:
Contract.objects.filter().delete()
if self.verbosity >= 1:
self.stdout.write(self.style.SUCCESS("Deleted all previous records."))
if self.verbosity >= 2:
self.stdout.write("\n")
self.import_all_contracts(git_dir)
if self.verbosity >= 1:
self.stdout.write(self.style.SUCCESS("\nGit repository sync complete."))
# Generated by Django 4.1.4 on 2023-04-20 09:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contracts", "0049_alter_contract_options"),
]
operations = [
migrations.AlterField(
model_name="contractfilingarea",
name="name",
field=models.CharField(max_length=128, verbose_name="Jméno"),
),
migrations.AlterField(
model_name="contractissue",
name="name",
field=models.CharField(max_length=128, verbose_name="Jméno"),
),
migrations.AlterField(
model_name="contracttype",
name="name",
field=models.CharField(max_length=128, verbose_name="Jméno"),
),
]
# Generated by Django 4.1.4 on 2023-04-20 09:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contracts", "0050_alter_contractfilingarea_name_and_more"),
]
operations = [
migrations.AlterField(
model_name="contract",
name="cost_unit_other",
field=models.CharField(
blank=True,
help_text='Je nutno vyplnit v případě, že máš vybranou možnost "jiné" v jednotce nákladů.',
max_length=256,
null=True,
verbose_name="Jednotka nákladů (jiné)",
),
),
migrations.AlterField(
model_name="contract",
name="id_number",
field=models.CharField(
blank=True,
max_length=256,
null=True,
verbose_name="Identifikační číslo",
),
),
migrations.AlterField(
model_name="contract",
name="name",
field=models.CharField(max_length=256, verbose_name="Název"),
),
migrations.AlterField(
model_name="contractapproval",
name="name",
field=models.CharField(max_length=256, verbose_name="Jméno"),
),
migrations.AlterField(
model_name="contractee",
name="department",
field=models.CharField(
blank=True, max_length=256, null=True, verbose_name="Organizační složka"
),
),
migrations.AlterField(
model_name="contractfile",
name="name",
field=models.CharField(
blank=True, max_length=256, null=True, verbose_name="Jméno"
),
),
migrations.AlterField(
model_name="contractfilingarea",
name="name",
field=models.CharField(max_length=256, verbose_name="Jméno"),
),
migrations.AlterField(
model_name="contractintent",
name="name",
field=models.CharField(max_length=256, verbose_name="Jméno"),
),
migrations.AlterField(
model_name="contractissue",
name="name",
field=models.CharField(max_length=256, verbose_name="Jméno"),
),
migrations.AlterField(
model_name="contracttype",
name="name",
field=models.CharField(max_length=256, verbose_name="Jméno"),
),
migrations.AlterField(
model_name="signee",
name="department",
field=models.CharField(
blank=True, max_length=256, null=True, verbose_name="Organizační složka"
),
),
]
# Generated by Django 4.1.4 on 2023-04-20 20:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contracts", "0051_alter_contract_cost_unit_other_and_more"),
]
operations = [
migrations.RemoveField(
model_name="contract",
name="legal_state",
),
migrations.AddField(
model_name="contract",
name="is_valid",
field=models.BooleanField(default=False, verbose_name="Je právně platná"),
),
]
......@@ -188,7 +188,7 @@ class Signee(
) # WARNING: Legal entity status dependent!
department = models.CharField(
max_length=128,
max_length=256,
blank=True,
null=True,
verbose_name="Organizační složka",
......@@ -341,7 +341,7 @@ class Contractee(
)
department = models.CharField(
max_length=128,
max_length=256,
blank=True,
null=True,
verbose_name="Organizační složka",
......@@ -370,7 +370,7 @@ class Contractee(
class ContractType(ContractCountMixin, NameStrMixin, models.Model):
name = models.CharField(
max_length=32,
max_length=256,
verbose_name="Jméno",
)
......@@ -387,7 +387,7 @@ class ContractType(ContractCountMixin, NameStrMixin, models.Model):
class ContractIssue(ContractCountMixin, NameStrMixin, models.Model):
name = models.CharField(
max_length=32,
max_length=256,
verbose_name="Jméno",
)
......@@ -404,7 +404,7 @@ class ContractIssue(ContractCountMixin, NameStrMixin, models.Model):
class ContractFilingArea(ContractCountMixin, NameStrMixin, models.Model):
name = models.CharField(
max_length=32,
max_length=256,
verbose_name="Jméno",
)
......@@ -490,12 +490,12 @@ class Contract(NameStrMixin, models.Model):
# END Approval fields
name = models.CharField(
max_length=128,
max_length=256,
verbose_name="Název",
)
id_number = models.CharField(
max_length=128,
max_length=256,
blank=True,
null=True,
verbose_name="Identifikační číslo",
......@@ -533,10 +533,6 @@ class Contract(NameStrMixin, models.Model):
),
)
class LegalStates(models.TextChoices):
VALID = "valid", "Platná"
INVALID = "invalid", "Neplatná"
class PublicStates(models.TextChoices):
YES = "yes", "Veřejná"
NO = "no", "Neveřejná"
......@@ -553,10 +549,9 @@ class Contract(NameStrMixin, models.Model):
LOST = "lost", "Ztracený"
legal_state = models.CharField(
max_length=13,
choices=LegalStates.choices,
verbose_name="Stav právního ujednání",
is_valid = models.BooleanField(
default=False,
verbose_name="Je právně platná",
)
is_public = models.BooleanField(
......@@ -622,7 +617,7 @@ class Contract(NameStrMixin, models.Model):
)
cost_unit_other = models.CharField(
max_length=128,
max_length=256,
verbose_name="Jednotka nákladů (jiné)",
help_text='Je nutno vyplnit v případě, že máš vybranou možnost "jiné" v jednotce nákladů.',
blank=True,
......@@ -825,7 +820,7 @@ def get_contract_file_loaction(instance, filename):
class ContractFile(NameStrMixin, models.Model):
name = models.CharField(
max_length=128,
max_length=256,
blank=True,
null=True,
verbose_name="Jméno",
......@@ -995,7 +990,7 @@ def signing_parties_post_save_update_dates(sender, instance, *args, **kwargs) ->
class ContractApproval(NameStrMixin, models.Model):
name = models.CharField(
max_length=128,
max_length=256,
verbose_name="Jméno",
)
......@@ -1024,7 +1019,7 @@ class ContractApproval(NameStrMixin, models.Model):
class ContractIntent(NameStrMixin, models.Model):
name = models.CharField(
max_length=128,
max_length=256,
verbose_name="Jméno",
)
......
{% extends "shared/includes/base.html" %}
{% block content %}
<h1 class="font-alt text-6xl mb-6 md:mb-10">
Přesun do nového Registru
</h1>
<div class="prose max-w-none">
<p>
{% if archive_page_exists %}
Tato smlouva bohužel zatím nebyla zadána do nedávno nasazené nové verze Registru
smluv.<br>
<strong>
Je však stále dostupná v archivu na adrese:
<a href="{{ archive_url }}">{{ archive_url }}</a>.
</strong>
{% else %}
Tato smlouva byla možná v určitou dobu dostupná ve staré verzi Registru smluv,
ale aktuálně není dostupná v ani jedné ze dvou verzí. Pokud víš jistě, že ji
dokážeš najít, archiv najdeš na
<a href="https://smlouvy-archiv.pirati.cz">smlouvy-archiv.pirati.cz</a>.
{% endif %}
</p>
<p>
Více informací o novém Registru se můžeš dozvědět
<a href="https://to.pirati.cz/aktuality/novy-registr-smluv/">zde v článku TO</a>.
</p>
</div>
{% endblock %}
......@@ -32,7 +32,10 @@
</ul>
</td>
<td{% if not contract.is_public %} class="!bg-red-100"{% endif %}>
<i class="{% if contract.legal_state == contract.LegalStates.VALID %}ico--checkmark{% else %}ico--cross{% endif %}"></i>
<i
class="{% if contract.is_valid %}ico--checkmark{% else %}ico--cross{% endif %}"
aria-label="{% if contract.is_public %}Ano{% else %}Ne{% endif %}"
></i>
</td>
<td class="whitespace-nowrap{% if not contract.is_public %} !bg-red-100{% endif %}">
{{ contract.valid_start_date }}
......@@ -109,7 +112,10 @@
<tr>
<td>Platná:</td>
<td>
<i class="{% if contract.legal_state == contract.LegalStates.VALID %}ico--checkmark{% else %}ico--cross{% endif %}"></i>
<i
class="{% if contract.is_valid %}ico--checkmark{% else %}ico--cross{% endif %}"
aria-label="{% if contract.is_public %}Ano{% else %}Ne{% endif %}"
></i>
</td>
</tr>
<tr>
......
......@@ -77,23 +77,12 @@
<td class="w-4/5 !p-2.5">{{ contract.summary }}</td>
</tr>
{% if user.can_view_confidential %}
<tr>
<td class="w-1/5 !p-2.5">Je veřejná</td>
<td class="w-4/5 !p-2.5">
<i
class="{% if contract.legal_state == contract.LegalStates.VALID %}ico--checkmark{% else %}ico--cross{% endif %} __tooltipped"
aria-label="{{ contract.get_legal_state_display }}"
></i>
</td>
</tr>
{% endif %}
<tr>
<td class="w-1/5 !p-2.5">Je platná</td>
<td class="w-4/5 !p-2.5">
<i
class="{% if contract.legal_state == contract.LegalStates.VALID %}ico--checkmark{% else %}ico--cross{% endif %} __tooltipped"
aria-label="{{ contract.get_legal_state_display }}"
class="{% if contract.is_valid %}ico--checkmark{% else %}ico--cross{% endif %} __tooltipped"
aria-label="{% if contract.is_valid %}Ano{% else %}Ne{% endif %}"
></i>
</td>
</tr>
......
import os
from xml.etree import ElementTree
import requests
......@@ -424,3 +426,25 @@ def get_ares_info(request, ico: int):
status=ares_info.status_code,
content_type=ares_info.headers.get("Content-Type"),
)
def handle_404(request, exception):
path = os.path.normpath(request.get_full_path())
archive_url = f"https://smlouvy-archiv.pirati.cz{path}"
archive_response = requests.get(archive_url)
# Quick dirty check for whether the page found is an actual contract
was_found = archive_response.ok and "Datum podpisu" in str(archive_response.content)
return render(
request,
"contracts/404.html",
{
**get_base_context(request),
"title": "Stránka nenalezena",
"description": "",
"archive_page_exists": was_found,
"archive_url": archive_url,
},
)
This diff is collapsed.
......@@ -31,3 +31,5 @@ urlpatterns = [
path("admin/", admin.site.urls),
path("settings/", include("dbsettings.urls")),
] + pirates_urlpatterns
handler404 = "contracts.views.handle_404"
......@@ -15,4 +15,7 @@ django-markdownx==4.0.0b1
django-environ==0.9.0
django-http-exceptions==1.4.0
django-guardian==2.4.0
GitPython==3.1.31
Markdown==3.4.3
PyJWT==2.6.0
PyYAML==6.0
......@@ -37,12 +37,12 @@
>
<link
href="https://styleguide.pirati.cz/2.11.x/css/styles.css"
href="https://styleguide.pirati.cz/2.12.x/css/styles.css"
rel="stylesheet"
media="all"
>
<link
href="https://styleguide.pirati.cz/2.11.x/css/pattern-scaffolding.css"
href="https://styleguide.pirati.cz/2.12.x/css/pattern-scaffolding.css"
rel="stylesheet"
media="all"
>
......@@ -142,13 +142,13 @@
<section class="footer__brand">
<a href="https://www.pirati.cz">
<img
src="https://styleguide.pirati.cz/2.11.x/images/logo-full-white.svg"
src="https://styleguide.pirati.cz/2.12.x/images/logo-full-white.svg"
alt="Logo Pirátské strany"
class="w-32 md:w-40 pb-6"
>
</a>
<p class="para mb-4 text-grey-200">
<span class="copyleft inline-block">©</span> 2023 Piráti.
<span class="copyleft inline-block">©</span> {% now "Y" %} Piráti.
Všechna práva vyhlazena.
Sdílejte a nechte ostatní sdílet za stejných podmínek.
</p>
......@@ -201,7 +201,7 @@
</footer>
<script
src="https://styleguide.pirati.cz/2.11.x/js/main.bundle.js"
src="https://styleguide.pirati.cz/2.12.x/js/main.bundle.js"
></script>
</body>
</html>
......@@ -30,7 +30,9 @@ def get_fieldsets_and_inlines(context):
(FieldsetInlineOrder.FIELDSET, next(adminform))
)
elif choice == FieldsetInlineOrder.INLINE:
fieldsets_and_inlines.append((FieldsetInlineOrder.INLINE, next(inlines)))
fieldsets_and_inlines.append(
(FieldsetInlineOrder.INLINE, next(inlines))
)
except StopIteration:
# Missing permissions, ignore and move on
continue
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment