Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/46 producttype nl datamodel #2

Merged
merged 8 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 273 additions & 85 deletions src/open_producten/conf/locale/en/LC_MESSAGES/django.po

Large diffs are not rendered by default.

377 changes: 279 additions & 98 deletions src/open_producten/conf/locale/nl/LC_MESSAGES/django.po

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/open_producten/producttypen/admin/bestand.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class BestandInline(admin.TabularInline):
class BestandAdmin(admin.ModelAdmin):
Floris272 marked this conversation as resolved.
Show resolved Hide resolved
list_display = ("product_type", "bestand")
list_filter = ("product_type",)
search_fields = ("bestand",)

def get_queryset(self, request):
return super().get_queryset(request).select_related("product_type")
2 changes: 2 additions & 0 deletions src/open_producten/producttypen/admin/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class LinkInline(admin.TabularInline):
@admin.register(Link)
class LinkAdmin(admin.ModelAdmin):
Floris272 marked this conversation as resolved.
Show resolved Hide resolved
list_display = ("product_type", "naam", "url")
list_filter = ("product_type__naam",)
search_fields = ("naam", "product_type__naam")

def get_queryset(self, request):
return super().get_queryset(request).select_related("product_type")
6 changes: 2 additions & 4 deletions src/open_producten/producttypen/admin/onderwerp.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def clean(self):
if not gepubliceerd and any([data[child] for child in children]):
raise forms.ValidationError(
_(
"Hoofd onderwerpen moeten gepubliceerd zijn met gepubliceerde sub onderwerpen."
"Onderwerpen moeten gepubliceerd zijn met gepubliceerde sub onderwerpen."
)
)

Expand Down Expand Up @@ -70,9 +70,7 @@ class OnderwerpAdmin(TreeAdmin):
),
)

list_filter = [
"gepubliceerd",
]
list_filter = ["gepubliceerd", "product_typen"]

def get_changelist_formset(self, request, **kwargs):
kwargs["formset"] = OnderwerpAdminFormSet
Expand Down
8 changes: 8 additions & 0 deletions src/open_producten/producttypen/admin/prijs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib import admin
from django.core.exceptions import ValidationError
from django.forms import BaseInlineFormSet
from django.utils.datetime_safe import datetime
from django.utils.translation import gettext_lazy as _

from ..models import Prijs, PrijsOptie
Expand Down Expand Up @@ -31,6 +32,13 @@ class PrijsOptieInline(admin.TabularInline):
class PrijsAdmin(admin.ModelAdmin):
model = Prijs
inlines = [PrijsOptieInline]
list_display = ("__str__", "actief_vanaf")
list_filter = ("product_type__naam", "actief_vanaf")

def get_queryset(self, request):
return super().get_queryset(request).select_related("product_type")

def has_change_permission(self, request, obj=None):
if obj and obj.actief_vanaf < datetime.today().date():
return False
return super().has_change_permission(request, obj)
Comment on lines +41 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still feel that this will come up as an issue in the future, but tomorrows problems are for tomorrow's me.

And by me I mean you.

13 changes: 10 additions & 3 deletions src/open_producten/producttypen/admin/producttype.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ def clean(self):

@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
list_display = ("naam", "aanmaak_datum", "display_onderwerpen", "gepubliceerd")
list_display = (
"naam",
"uniforme_product_naam",
"aanmaak_datum",
"display_onderwerpen",
"gepubliceerd",
"keywords",
)
list_filter = ("gepubliceerd", "onderwerpen")
list_editable = ("gepubliceerd",)
date_hierarchy = "aanmaak_datum"
Expand All @@ -39,7 +46,7 @@ class ProductTypeAdmin(admin.ModelAdmin):
# "contacts",
# "locations",
)
search_fields = ("naam",)
search_fields = ("naam", "uniforme_product_naam__naam", "keywords")
ordering = ("naam",)
save_on_top = True
form = ProductTypeAdminForm
Expand All @@ -57,4 +64,4 @@ def get_queryset(self, request):

@admin.display(description="onderwerpen")
def display_onderwerpen(self, obj):
return ", ".join(p.naam for p in obj.onderwerp.all())
return ", ".join(p.naam for p in obj.onderwerpen.all())
123 changes: 96 additions & 27 deletions src/open_producten/producttypen/management/commands/load_upl.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,137 @@
import csv
import os
from dataclasses import dataclass
from io import StringIO

from django.core.management.base import BaseCommand, CommandError
from django.db import transaction

import requests

from open_producten.producttypen.models import (
UniformeProductNaam as UniformProductNaamModel,
)

from ..parsers import CsvParser


@dataclass
class UniformProductName:
name: str
uri: str


def _check_if_csv_extension(path: str):
_, extension = os.path.splitext(path)
file_format = extension[1:]

if file_format != "csv":
raise CommandError("File format is not csv.")


class Command(BaseCommand):
Floris272 marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self):
self.help = "Load upn to the database from a given XML/CSV file."
self.parser = CsvParser()
self.help = (
"Load upn to the database from a given local csv file or csv file url."
)
super().__init__()

def add_arguments(self, parser):
parser.add_argument(
"filename",
help="The name of the file to be imported.",
"--file",
help="The path to the csv file to be imported.",
)
parser.add_argument(
"--url",
help="The url to the csv file to be imported.",
)

def handle(self, **options):
filename = options.pop("filename")
file = options.pop("file")
url = options.pop("url")

if file and url:
raise CommandError("Only one of --file or --url can be specified.")

self.stdout.write(f"Importing upn from {filename}...")
if not file and not url:
raise CommandError("Either --file or --url must be specified.")

self.stdout.write(f"Importing upn from {file if file else url}...")

try:
data = self.parser.parse(filename)
upn_objects = [
UniformProductName(name=entry["UniformeProductnaam"], uri=entry["URI"])
for entry in data
]
created_count = self.load_upl(upn_objects)

except KeyError as e:
raise CommandError(f"{str(e)} does not exist in csv.")
if file:
created_count, update_count, removed_count = self._parse_csv_file(file)
else:
created_count, update_count, removed_count = self._parse_csv_url(url)
except CommandError:
raise
except Exception as e:
raise CommandError(str(e))
raise CommandError(f"Something went wrong: {str(e)}")
self.stdout.write(
"Done\n"
f"Created {created_count} product names.\n"
f"Updated {update_count} product names.\n"
f"{removed_count} product names did not exist in the csv."
)

def _parse_csv_file(self, file: str):
_check_if_csv_extension(file)

with open(file, encoding="utf-8-sig") as f:
data = csv.DictReader(f)
return self._load_upl(data)

def _parse_csv_url(self, url: str):
_check_if_csv_extension(url)

self.stdout.write(f"Done ({created_count} objects).")
response = requests.get(url)
if response.status_code != 200:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you intended it, but this will not catch 400 or 500 codes, only different 200 and 300 codes

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requests throws exceptions for those codes

raise CommandError(f"Url returned status code: {response.status_code}.")

def load_upl(self, data: list[UniformProductName]) -> int:
count = 0
content = StringIO(response.text)
data = csv.DictReader(content)
return self._load_upl(data)

@transaction.atomic
def _load_upl(self, data: csv.DictReader) -> tuple[int, int, int]:
created_count = 0
updated_count = 0
upn_updated_list = []
columns = {
"uri": "URI",
"name": "UniformeProductnaam",
}

if missing_columns := [
f"'{key}'" for key in columns.values() if key not in data.fieldnames
]:
raise CommandError(
f"Column(s) {', '.join(missing_columns)} do not exist in the CSV."
)

for i, row in enumerate(data):
uri = row[columns["uri"]]
name = row[columns["name"]]

if not name or not uri:
self.stdout.write(
f"Skipping index {i} because of missing column(s) {' or '.join(columns.values())}."
)
continue

for obj in data:
upn, created = UniformProductNaamModel.objects.update_or_create(
Floris272 marked this conversation as resolved.
Show resolved Hide resolved
uri=obj.uri,
defaults={"naam": obj.name, "is_verwijderd": False},
uri=uri,
defaults={"naam": name, "is_verwijderd": False},
)
upn_updated_list.append(upn.id)

if created:
count += 1
created_count += 1
else:
updated_count += 1

UniformProductNaamModel.objects.exclude(id__in=upn_updated_list).update(
removed_count = UniformProductNaamModel.objects.exclude(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets the count but IDK how. Should it not return a queryset? or is str(querset) always the count?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.update() returns the number of affected rows.
docs

id__in=upn_updated_list
).update(
is_verwijderd=True,
)
return count
return created_count, updated_count, removed_count
29 changes: 0 additions & 29 deletions src/open_producten/producttypen/management/parsers.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Generated by Django 4.2.16 on 2024-12-09 15:38

import datetime
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import markdownx.models


class Migration(migrations.Migration):

dependencies = [
("producttypen", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="onderwerp",
name="beschrijving",
field=markdownx.models.MarkdownxField(
blank=True,
default="",
help_text="Beschrijving van het onderwerp, ondersteund markdown format.",
verbose_name="beschrijving",
),
),
migrations.AlterField(
model_name="prijs",
name="actief_vanaf",
field=models.DateField(
help_text="De datum vanaf wanneer de prijs actief is.",
validators=[
django.core.validators.MinValueValidator(datetime.date.today)
],
verbose_name="start datum",
),
),
migrations.AlterField(
model_name="producttype",
name="beschrijving",
field=markdownx.models.MarkdownxField(
help_text="Product type beschrijving, ondersteund markdown format.",
verbose_name="beschrijving",
),
),
migrations.AlterField(
model_name="producttype",
name="naam",
field=models.CharField(
help_text="naam van het product type.",
max_length=100,
verbose_name="product type naam",
),
),
migrations.AlterField(
model_name="vraag",
name="antwoord",
field=markdownx.models.MarkdownxField(
help_text="Het antwoord op de vraag, ondersteund markdown format.",
verbose_name="antwoord",
),
),
migrations.AlterField(
model_name="vraag",
name="onderwerp",
field=models.ForeignKey(
blank=True,
help_text="Het onderwerp waarbij deze vraag hoort.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="vragen",
to="producttypen.onderwerp",
verbose_name="onderwerp",
),
),
migrations.AlterField(
model_name="vraag",
name="product_type",
field=models.ForeignKey(
blank=True,
help_text="Het product type waarbij deze vraag hoort.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="vragen",
to="producttypen.producttype",
verbose_name="Product type",
),
),
migrations.AlterField(
model_name="vraag",
name="vraag",
field=models.CharField(
help_text="De vraag die wordt beantwoord.",
max_length=250,
verbose_name="vraag",
),
),
]
Loading
Loading