From 901e886be0e93461077e20d64eb2896b3350e781 Mon Sep 17 00:00:00 2001 From: Vincent Jousse Date: Wed, 5 Jun 2024 15:28:00 +0200 Subject: [PATCH] Update backend to latest developments --- Pipfile | 2 +- Pipfile.lock | 16 +- backend/authentication/init.py | 29 +- backend/authentication/models.py | 10 +- backend/backend/settings.py | 3 +- .../admin/textile/example/change_list.html | 11 + .../admin/textile/example/from_json.html | 13 + backend/textile/admin.py | 223 +++++++++- backend/textile/choices.py | 14 +- backend/textile/init.py | 244 +++-------- backend/textile/models.py | 383 +++++++++++++----- backend/update.sh | 6 +- 12 files changed, 607 insertions(+), 347 deletions(-) create mode 100644 backend/templates/admin/textile/example/change_list.html create mode 100644 backend/templates/admin/textile/example/from_json.html diff --git a/Pipfile b/Pipfile index 0d2363938..5fe20b849 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] django = ">=5,<6" django-mail-auth = ">=3.2,<3.3" -gunicorn = ">=21,<22" +gunicorn = ">=22,<23" psycopg = ">=3.1,<3.2" python-decouple = ">=3,<4" ruff = "*" diff --git a/Pipfile.lock b/Pipfile.lock index f4b1ede99..d01877ef4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "98813e8d072ccbb0aff413cf909a77b2e126646473200d3888a3b24a6eb14f20" + "sha256": "da2f82edb0868b3cfde8e0a23fb223da1851cdd20569cc150da7f9b1c6ded4e2" }, "pipfile-spec": 6, "requires": {}, @@ -65,12 +65,12 @@ }, "gunicorn": { "hashes": [ - "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", - "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", + "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63" ], "index": "pypi", - "markers": "python_version >= '3.5'", - "version": "==21.2.0" + "markers": "python_version >= '3.7'", + "version": "==22.0.0" }, "identify": { "hashes": [ @@ -82,11 +82,11 @@ }, "nodeenv": { "hashes": [ - "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1", - "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a" + "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", + "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.9.0" + "version": "==1.9.1" }, "packaging": { "hashes": [ diff --git a/backend/authentication/init.py b/backend/authentication/init.py index b9c832e1f..c6bf8a8a0 100644 --- a/backend/authentication/init.py +++ b/backend/authentication/init.py @@ -3,7 +3,28 @@ def init(): - # create initial admins given by an env var. Mails separated by comma - for email in [m.strip() for m in config("BACKEND_ADMINS", "").split(",")]: - if not get_user_model().objects.filter(email=email): - get_user_model().objects.create_superuser(email, terms_of_use=True) + # create initial admins given by an env var. Mails separated by comma, with optional token + # So the env var can be in the form: user@example.com=ABCDEFGH,user2@example.com,user3@example.com + # this allows a user to have a persistent token among all the deployments + for admin in [ + m.strip().split("=") for m in config("BACKEND_ADMINS", "").split(",") + ]: + if len(admin) > 1: + # we specified the mail and token + (email, token) = admin + if not get_user_model().objects.filter(email=email): + # user not found by mail, create it + get_user_model().objects.create_superuser( + email, terms_of_use=True, token=token + ) + elif not get_user_model().objects.filter(email=email, token=token): + # user found by mail but not by token, update the token + user = get_user_model().objects.filter(email=email).first() + if user: + user.token = token + user.save() + else: + # we specified only the mail + (email,) = admin + if not get_user_model().objects.filter(email=email): + get_user_model().objects.create_superuser(email, terms_of_use=True) diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 83849980c..3bcbd971c 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -1,15 +1,13 @@ import uuid -from django.db import models +from django.db.models import BooleanField, CharField from django.utils.translation import gettext_lazy as _ from mailauth.contrib.user.models import AbstractEmailUser class EcobalyseUser(AbstractEmailUser): - organization = models.CharField( - _("Organization"), max_length=150, blank=True, default="" - ) - terms_of_use = models.BooleanField(default=False) - token = models.CharField( + organization = CharField(_("Organization"), max_length=150, blank=True, default="") + terms_of_use = BooleanField(default=False) + token = CharField( _("TOKEN"), max_length=36, default=uuid.uuid4, editable=False, db_index=True ) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 0df3d0260..cf2187508 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -50,8 +50,7 @@ # # don't use the provided mailauth user, it's redefined in the authentication module # "mailauth.contrib.user", "authentication.apps.AuthenticationConfig", - # # disable textile for now - # "textile.apps.TextileConfig", + # "textile.apps.TextileConfig", # TODO disable textile for now # # the original admin config is replaced by custom AdminConfig # "django.contrib.admin", "backend.apps.AdminConfig", diff --git a/backend/templates/admin/textile/example/change_list.html b/backend/templates/admin/textile/example/change_list.html new file mode 100644 index 000000000..e7786422b --- /dev/null +++ b/backend/templates/admin/textile/example/change_list.html @@ -0,0 +1,11 @@ +{% extends "admin/change_list.html" %} +{% load i18n static %} + +{% block object-tools-items %} + {{ block.super }} +
  • + + {% trans "Create from JSON" %} + +
  • +{% endblock %} diff --git a/backend/templates/admin/textile/example/from_json.html b/backend/templates/admin/textile/example/from_json.html new file mode 100644 index 000000000..45e142be1 --- /dev/null +++ b/backend/templates/admin/textile/example/from_json.html @@ -0,0 +1,13 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} +{% block content %} +

    {% trans "Create the Example from the provided JSON block in Ecobalyse" %}

    +
    +
    + {% csrf_token %} + {{ form.as_p }} + + +
    +
    +{% endblock %} diff --git a/backend/textile/admin.py b/backend/textile/admin.py index 58a55833d..4dda779bf 100644 --- a/backend/textile/admin.py +++ b/backend/textile/admin.py @@ -1,11 +1,55 @@ -from django.contrib import admin +import json + +from django import forms +from django.contrib import admin, messages +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.urls import path, reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ from backend.admin import admin_site from textile.models import Example, Material, Process, Product class ProductAdmin(admin.ModelAdmin): + save_on_top = True search_fields = ["name"] + list_display = ("name", "id", "mass", "volume") + fieldsets = [ + (None, {"fields": ("name", "id", "mass", "surfaceMass", "yarnSize", "fabric")}), + ( + _("Economics"), + { + "fields": ( + "business", + "marketingDuration", + "numberOfReferences", + "price", + "repairCost", + "traceability", + ) + }, + ), + (_("Dyeing"), {"fields": ("defaultMedium",)}), + (_("Making"), {"fields": ("pcrWaste", "complexity")}), + ( + _("Use"), + { + "fields": ( + "ironingElecInMJ", + "nonIroningProcessUuid", + "daysOfWear", + "defaultNbCycles", + "ratioDryer", + "ratioIroning", + "timeIroning", + "wearsPerCycle", + ) + }, + ), + (_("End Of Life"), {"fields": ("volume",)}), + ] class MaterialsInline(admin.TabularInline): @@ -13,20 +57,191 @@ class MaterialsInline(admin.TabularInline): class MaterialAdmin(admin.ModelAdmin): + save_on_top = True search_fields = ["name"] - # inlines = [MaterialsInline] + list_display = ("name", "shortName", "id", "related_process") + + def related_process(self, obj): + url = reverse( + "admin:textile_process_change", args=(obj.materialProcessUuid.pk,) + ) + return format_html(f'{obj.materialProcessUuid.name}') + + related_process.allow_tags = True + fieldsets = [ + (None, {"fields": ("name", "shortName", "origin", "priority")}), + ( + _("Processes"), + {"fields": ("materialProcessUuid", "recycledProcessUuid", "recycledFrom")}, + ), + (_("Geography"), {"fields": ("geographicOrigin", "defaultCountry")}), + (_("Other"), {"fields": ("manufacturerAllocation", "recycledQualityRatio")}), + ] class ProcessAdmin(admin.ModelAdmin): + save_on_top = True search_fields = ["name"] + list_display = ("name", "source", "uuid", "step_usage") + fieldsets = [ + ( + None, + { + "fields": ( + "name", + "uuid", + "source", + "search", + "info", + "unit", + "step_usage", + "correctif", + ) + }, + ), + (_("Energy"), {"fields": ("heat_MJ", "elec_pppm", "elec_MJ")}), + (_("Scores"), {"fields": ("pef", "ecs")}), + ( + _("Impacts"), + { + "fields": ( + "acd", + "cch", + "etf", + "etfc", + "fru", + "fwe", + "htc", + "htcc", + "htn", + "htnc", + "ior", + "ldu", + "mru", + "ozd", + "pco", + "pma", + "swe", + "tre", + "wtu", + ) + }, + ), + ] + + +class ExampleJSONForm(forms.ModelForm): + class Meta: + model = Example + fields = ["id", "name", "product"] + query = forms.JSONField() -class ExempleAdmin(admin.ModelAdmin): + +class ExampleAdmin(admin.ModelAdmin): search_fields = ["name"] inlines = [MaterialsInline] + change_list_template = "admin/textile/example/change_list.html" + save_on_top = True + list_display = ("name", "id", "product") + fieldsets = [ + (None, {"fields": ["id", "name", "mass"]}), + ( + "Durabilité non-physique", + { + "fields": [ + "product", + "numberOfReferences", + "price", + "marketingDuration", + "business", + "traceability", + "repairCost", + ], + "description": "Paramètres de durabilité non-physique. Voir la Documentation", + }, + ), + ( + "Filature", + { + "fields": [ + "countrySpinning", + ], + "description": "Documentation", + }, + ), + ( + "Fabrication", + { + "fields": [ + "fabricProcess", + "countryFabric", + ], + "description": "Documentation", + }, + ), + ( + "Confection", + { + "fields": [ + "airTransportRatio", + "countryMaking", + ], + "description": "Documentation", + }, + ), + ( + "Ennoblissement", + { + "fields": [ + "countryDyeing", + ], + "description": "Documentation", + }, + ), + ] + + def get_urls(self): + return [ + path( + "from-json/", + self.admin_site.admin_view(self.from_json), + name="from-json", + ) + ] + super().get_urls() + + def from_json(self, request): + """/admin/textile/example/from-json/ form""" + if request.method == "POST": + form = ExampleJSONForm(request.POST) + if form.is_valid(): + json_example = { + "id": request.POST["id"], + "name": request.POST["name"], + "product": request.POST["product"], + "query": json.loads(request.POST["query"]), + } + try: + example = Example._fromJSON(json_example) + example.save() + + for share in json_example["query"]["materials"]: + example.add_material(share) + self.message_user(request, _("Your Example has been recorded")) + return HttpResponseRedirect("..") + except TypeError: + self.message_user( + request, + _("Your JSON doesn't look like a valid example"), + level=messages.ERROR, + ) + else: + form = ExampleJSONForm() + context = dict(self.admin_site.each_context(request), form=form) + return render(request, "admin/textile/example/from_json.html", context) admin_site.register(Product, ProductAdmin) admin_site.register(Material, MaterialAdmin) admin_site.register(Process, ProcessAdmin) -admin_site.register(Example, ExempleAdmin) +admin_site.register(Example, ExampleAdmin) diff --git a/backend/textile/choices.py b/backend/textile/choices.py index 8f33f7437..af45cdc92 100644 --- a/backend/textile/choices.py +++ b/backend/textile/choices.py @@ -17,7 +17,7 @@ "large-business-without-services": "Grande entreprise ne proposant pas de service de réparation ou de garantie", } DYEINGMEDIA = {"article": "Article", "fabric": "Tissu", "yarn": "Fil"} -MAXKINGCOMPLEXITIES = { +MAKINGCOMPLEXITIES = { "very-high": "Très élevée", "high": "Élevée", "medium": "Moyenne", @@ -57,15 +57,3 @@ "Utilisation", ] } -CATEGORIES = { - k: k - for k in [ - "Chemise", - "Jean", - "Jupe / Robe", - "Manteau / Veste", - "Pantalon / Short", - "Pull / Couche intermédiaire", - "Tshirt / Polo", - ] -} diff --git a/backend/textile/init.py b/backend/textile/init.py index c384052f6..f15ff770d 100644 --- a/backend/textile/init.py +++ b/backend/textile/init.py @@ -1,214 +1,66 @@ import json -import sys -from copy import deepcopy from os.path import join from django.conf import settings -from textile.models import Example, Material, Process, Product, Share +from textile.models import ( + Example, + Material, + Process, + Product, +) - -def flatten(field, record): - """take a record and flatten the given fields - >>> flatten('b', {a: 1, b: {c: 2, d: 3}}) - {a: 1, c: 2, d: 3} - """ - if field in record: - if record.get(field): - record.update(record[field]) - del record[field] - - return record - - -def delchar(char, record): - """remove invalid char from dict keys - >>> delchar('-', {htn-c: 0, htc-c: 0}) - {htnc: 0, htcc: 0} - """ - return {k.replace(char, ""): v for k, v in record.items()} - - -def delkey(key, record): - """remove key from dict. The key may be dotted to delete a subfield: - >>> delkey('a.b', {'a': {'b': 1, 'c': 2}}) - 'a': {'c': 2}} - """ - k = key.split(".")[-1] - path = list(reversed(key.split(".")[:-1])) - d = record - while len(path): - d = d.get(path.pop()) - if k in d: - del d[k] - return record +TEXTILE_PATH = join(settings.GITROOT, "public", "data", "textile") def init(): """populate the db with initial admins and public json data""" - # stop if the database is not sqlite3 or is already populated (just check the number of users) - if ( - "sqlite3" not in settings.DATABASES.get("default", {}).get("ENGINE", "") - or Process.objects.count() <= 0 - ): - sys.exit() - - # return # FIXME don't load textile data yet - # PROCESSES - with open( - join( - settings.GITROOT, - "public", - "data", - "textile", - "processes_impacts.json", - ) - ) as f: - processes = json.load(f) - Process.objects.bulk_create( - [ - Process(**delkey("bvi", delchar("-", flatten("impacts", deepcopy(p))))) - for p in processes - ] - ) + if Process.objects.count() == 0: + with open(join(TEXTILE_PATH, "processes_impacts.json")) as f: + Process.objects.bulk_create([Process._fromJSON(p) for p in json.load(f)]) + else: + print("Processes already loaded") # MATERIALS - with open( - join( - settings.GITROOT, - "public", - "data", - "textile", - "materials.json", - ) - ) as f: - materials = json.load(f) - # all fields except the foreignkeys - Material.objects.bulk_create( - [ - Material( - **delkey( - "recycledFrom", - delkey( - "materialProcessUuid", - delkey( - "recycledProcessUuid", - delkey("primary", flatten("cff", deepcopy(m))), - ), - ), - ) - ) - for m in materials - ] - ) - # update with recycledFrom - mobjects = [Material.objects.get(pk=m["id"]) for m in materials] - recycledFroms = {m["id"]: m.get("recycledFrom") for m in materials} - materialProcesses = {m["id"]: m.get("materialProcessUuid") for m in materials} - recycledProcesses = {m["id"]: m.get("recycledProcessUuid") for m in materials} - for m in mobjects: - m.recycledFrom = ( - Material.objects.get(pk=recycledFroms[m.id]) - if recycledFroms[m.id] - else None - ) - m.materialProcessUuid = ( - Process.objects.get(pk=materialProcesses[m.id]) - if materialProcesses[m.id] - else None - ) - m.recycledProcessUuid = ( - Process.objects.get(pk=recycledProcesses[m.id]) - if recycledProcesses[m.id] - else None - ) - Material.objects.bulk_update( - mobjects, ["recycledFrom", "materialProcessUuid", "recycledProcessUuid"] - ) + if Material.objects.count() == 0: + with open(join(TEXTILE_PATH, "materials.json")) as f: + materials = json.load(f) + Material.objects.bulk_create([Material._fromJSON(m) for m in materials]) + # update with recursive FKs + for material in materials: + if material["recycledFrom"]: + m = Material.objects.get(pk=material["id"]) + m.recycledFrom = Material.objects.get(pk=material["recycledFrom"]) + m.save() + else: + print("Materials already loaded") # PRODUCTS - with open( - join( - settings.GITROOT, - "public", - "data", - "textile", - "products.json", - ) - ) as f: - products = json.load(f) - Product.objects.bulk_create( - [ - Product( - **flatten( - "endOfLife", - flatten( - "use", - flatten( - "making", - flatten( - "dyeing", - flatten( - "economics", - delkey( - "use.nonIroningProcessUuid", deepcopy(p) - ), - ), - ), - ), - ), - ) - ) - for p in products - ] - ) - pobjects = [Product.objects.get(pk=p["id"]) for p in products] - nonIroningProcesses = {p["id"]: p["use"]["nonIroningProcessUuid"] for p in products} - for p in pobjects: - p.nonIroningProcessUuid = Process.objects.get(pk=nonIroningProcesses[p.id]) - Product.objects.bulk_update(pobjects, ["nonIroningProcessUuid"]) + if Product.objects.count() == 0: + with open(join(TEXTILE_PATH, "products.json")) as f: + products = json.load(f) + # product without FK + Product.objects.bulk_create([Product._fromJSON(p) for p in products]) + for p in products: + # update with FK + Product.objects.get(pk=p["id"]).nonIroningProcessUuid = Process.objects.get( + pk=p["use"]["nonIroningProcessUuid"] + ) + else: + print("Products already loaded") # EXAMPLES - with open( - join( - settings.GITROOT, - "public", - "data", - "textile", - "examples.json", - ) - ) as f: - examples = json.load(f) - # all fields except the foreignkeys - Example.objects.bulk_create( - [ - Example( - **delkey( - "materials", - delkey( - "fabricProcess", - delkey("product", flatten("query", deepcopy(e))), - ), - ) - ) - for e in examples - ] - ) - # update with product, materials and fabricProcess - eobjects = [Example.objects.get(pk=m["id"]) for m in examples] - products = {e["id"]: e["query"]["product"] for e in examples} - mobjects = [Material.objects.get(pk=m["id"]) for m in materials] - fabricProcesses = {m["id"]: m["query"]["fabricProcess"] for m in examples} - for e in eobjects: - e.product = Product.objects.get(pk=products[e.id]) - e.fabricProcess = Process.objects.get(alias=fabricProcesses[e.id]) - for example in examples: - for share in example["query"]["materials"]: - Share.objects.create( - example=Example.objects.get(pk=example["id"]), - material=Material.objects.get(pk=share["id"]), - share=share["share"], - ) - Example.objects.bulk_update(eobjects, ["product", "fabricProcess"]) + if Example.objects.count() == 0: + with open(join(TEXTILE_PATH, "examples.json")) as f: + examples = json.load(f) + # all fields except the m2m + Example.objects.bulk_create([Example._fromJSON(e) for e in examples]) + # create the m2m intermediary records + for e in examples: + for s in e["query"]["materials"]: + Example.objects.get(pk=e["id"]).add_material(s) + Example + else: + print("Examples already loaded") diff --git a/backend/textile/models.py b/backend/textile/models.py index 95a3a8a7d..acb9d4af1 100644 --- a/backend/textile/models.py +++ b/backend/textile/models.py @@ -1,63 +1,121 @@ import json +from copy import deepcopy -from django.db import models +from django.db.models import ( + CASCADE, + SET_NULL, + BooleanField, + CharField, + FloatField, + ForeignKey, + IntegerField, + ManyToManyField, + Model, +) +from django.utils.translation import gettext_lazy as _ from .choices import ( BUSINESSES, - CATEGORIES, COUNTRIES, DYEINGMEDIA, FABRICS, - MAXKINGCOMPLEXITIES, + MAKINGCOMPLEXITIES, ORIGINS, STEPUSAGES, UNITS, ) + +def flatten(field, record): + """take a record and flatten the given fields + >>> flatten('b', {a: 1, b: {c: 2, d: 3}}) + {a: 1, c: 2, d: 3} + """ + if field in record: + if record.get(field): + record.update(record[field]) + del record[field] + + return record + + +def delchar(char, record): + """remove invalid char from dict keys + >>> delchar('-', {htn-c: 0, htc-c: 0}) + {htnc: 0, htcc: 0} + """ + return {k.replace(char, ""): v for k, v in record.items()} + + +def delkey(key, record): + """remove key from dict. The key may be dotted to delete a subfield: + >>> delkey('a.b', {'a': {'b': 1, 'c': 2}}) + 'a': {'c': 2}} + """ + k = key.split(".")[-1] + path = list(reversed(key.split(".")[:-1])) + d = record + while len(path): + d = d.get(path.pop()) + if k in d: + del d[k] + return record + + # textile -class Process(models.Model): - search = models.CharField(max_length=200, blank=True) - name = models.CharField(max_length=200) - source = models.CharField(max_length=200) - info = models.CharField(max_length=200) - unit = models.CharField(max_length=50, choices=UNITS) - uuid = models.CharField(max_length=50, primary_key=True) - acd = models.FloatField() - cch = models.FloatField() - etf = models.FloatField() - etfc = models.FloatField() - fru = models.FloatField() - fwe = models.FloatField() - htc = models.FloatField() - htcc = models.FloatField() - htn = models.FloatField() - htnc = models.FloatField() - ior = models.FloatField() - ldu = models.FloatField() - mru = models.FloatField() - ozd = models.FloatField() - pco = models.FloatField() - pma = models.FloatField() - swe = models.FloatField() - tre = models.FloatField() - wtu = models.FloatField() - pef = models.FloatField() - ecs = models.FloatField() - heat_MJ = models.FloatField(default=0) - elec_pppm = models.FloatField() - elec_MJ = models.FloatField() - waste = models.FloatField() - alias = models.CharField(max_length=50, null=True) - step_usage = models.CharField(max_length=50, choices=STEPUSAGES) - correctif = models.CharField(max_length=200) +class Process(Model): + class Meta: + verbose_name_plural = "Processes" + + search = CharField(_("Brightway Search term"), max_length=200, blank=True) + name = CharField(_("Name"), max_length=200) + source = CharField(_("Source Database"), max_length=200) + info = CharField(_("Informations"), max_length=200) + unit = CharField(_("Unit"), max_length=50, choices=UNITS) + uuid = CharField("UUID", max_length=50, primary_key=True) + acd = FloatField(_("Acidification (acd)")) + cch = FloatField(_("Climate Change (cch)")) + etf = FloatField(_("Ecotoxicity, freshwater (etf)")) + etfc = FloatField(_("Ecotoxicity, freshwater, corrected (etf-c)")) + fru = FloatField(_("Resource use, fossils (fru)")) + fwe = FloatField(_("Eutrophication, freshwater (fwe)")) + htc = FloatField(_("Human toxicity, cancer (htc)")) + htcc = FloatField(_("Human toxicity, cancer, corrected (htc-c)")) + htn = FloatField(_("Human toxicity, non-cancer (htn)")) + htnc = FloatField(_("Human toxicity, non-cancer, corrected (htn-c)")) + ior = FloatField(_("Ionising radiation (ior)")) + ldu = FloatField(_("Land use (ldu)")) + mru = FloatField(_("Resource use, minerals and metals (mru)")) + ozd = FloatField(_("Ozone depletion (ozd)")) + pco = FloatField(_("Photochemical ozone formation (pco)")) + pma = FloatField(_("Particulate matter (pma)")) + swe = FloatField(_("Eutrophication, marine (swe)")) + tre = FloatField(_("Eutrophication, terrestrial (tre)")) + wtu = FloatField(_("Water use")) + pef = FloatField(_("PEF Score")) + ecs = FloatField(_("Environmental Cost")) + heat_MJ = FloatField(_("Heat MJ"), default=0) + elec_pppm = FloatField(_("Elec pppm")) + elec_MJ = FloatField(_("Elec MJ")) + waste = FloatField(_("Waste")) + alias = CharField(_("Alias"), max_length=50, null=True) + step_usage = CharField(_("Step Usage"), max_length=50, choices=STEPUSAGES) + correctif = CharField(_("Correction"), max_length=200) def __str__(self): return self.name @classmethod - def toJson(cls): + def _fromJSON(cls, process): + """Takes a json of a process, returns a Process instance""" + return Process( + **delkey("bvi", delchar("-", flatten("impacts", deepcopy(process)))) + ) + + @classmethod + def allToJSON(cls): return json.dumps( [ { @@ -102,44 +160,71 @@ def toJson(cls): ) -class Product(models.Model): - id = models.CharField(max_length=50, primary_key=True) - name = models.CharField(max_length=200) - mass = models.FloatField() - surfaceMass = models.FloatField() - yarnSize = models.FloatField() - fabric = models.CharField(max_length=50, choices=FABRICS) +class Product(Model): + id = CharField("ID", max_length=50, primary_key=True) + name = CharField(_("Name"), max_length=200) + mass = FloatField(_("Mass")) + surfaceMass = FloatField(_("Surface Mass")) + yarnSize = FloatField(_("Yarn Size")) + fabric = CharField(_("Fabric"), max_length=50, choices=FABRICS) # economics - business = models.CharField(max_length=50, choices=BUSINESSES) - marketingDuration = models.FloatField() - numberOfReferences = models.IntegerField() - price = models.FloatField() - repairCost = models.FloatField() - traceability = models.BooleanField() + business = CharField(_("Business Type"), max_length=50, choices=BUSINESSES) + marketingDuration = FloatField(_("Marketing Duration")) + numberOfReferences = IntegerField(_("Nb Of Recerences")) + price = FloatField(_("Price")) + repairCost = FloatField(_("Repair Cost")) + traceability = BooleanField(_("Traceability")) # dyeing - defaultMedium = models.CharField(max_length=50, choices=DYEINGMEDIA) + defaultMedium = CharField(_("Default Medium"), max_length=50, choices=DYEINGMEDIA) # making - pcrWaste = models.FloatField() - complexity = models.CharField(max_length=50, choices=MAXKINGCOMPLEXITIES) + pcrWaste = FloatField(_("PCR Waste")) + complexity = CharField(_("Complexity"), max_length=50, choices=MAKINGCOMPLEXITIES) # use - ironingElecInMJ = models.FloatField() - nonIroningProcessUuid = models.ForeignKey( - Process, on_delete=models.SET_NULL, null=True, related_name="productsNonIroning" + ironingElecInMJ = FloatField(_("Ironing Elec in MJ")) + nonIroningProcessUuid = ForeignKey( + Process, SET_NULL, null=True, related_name="productsNonIroning" ) - daysOfWear = models.IntegerField() - defaultNbCycles = models.IntegerField() - ratioDryer = models.FloatField() - ratioIroning = models.FloatField() - timeIroning = models.FloatField() - wearsPerCycle = models.FloatField() + daysOfWear = IntegerField(_("Days Of Wear")) + defaultNbCycles = IntegerField(_("Default Nb Of Cycles")) + ratioDryer = FloatField(_("Ratio Dryer")) + ratioIroning = FloatField(_("Ratio Ironing")) + timeIroning = FloatField(_("Time Ironing")) + wearsPerCycle = FloatField(_("Wears Per Cycle")) # enf of life - volume = models.FloatField() + volume = FloatField() def __str__(self): return self.name @classmethod - def toJson(cls): + def _fromJSON(cls, product): + """takes a json of a product, return an instance of Product""" + # all fields except the foreignkeys + p = Product( + **flatten( + "endOfLife", + flatten( + "use", + flatten( + "making", + flatten( + "dyeing", + flatten( + "economics", + delkey("use.nonIroningProcessUuid", deepcopy(product)), + ), + ), + ), + ), + ) + ) + p.nonIroningProcessUuid = Process.objects.get( + pk=product["use"]["nonIroningProcessUuid"] + ) + return p + + @classmethod + def allToJSON(cls): return json.dumps( [ { @@ -182,32 +267,71 @@ def toJson(cls): ) -class Material(models.Model): - id = models.CharField(max_length=50, primary_key=True) - materialProcessUuid = models.ForeignKey( - Process, on_delete=models.SET_NULL, null=True, related_name="materials" +class Material(Model): + id = CharField("ID", max_length=50, primary_key=True) + materialProcessUuid = ForeignKey( + Process, + SET_NULL, + null=True, + related_name="materials", + verbose_name=_("Process"), ) - recycledProcessUuid = models.ForeignKey( - Process, on_delete=models.SET_NULL, null=True, related_name="recycledMaterials" + recycledProcessUuid = ForeignKey( + Process, + SET_NULL, + null=True, + related_name="recycledMaterials", + verbose_name=_("Recycled Process"), ) - recycledFrom = models.ForeignKey( - "self", null=True, on_delete=models.SET_NULL, blank=True + recycledFrom = ForeignKey( + "self", SET_NULL, null=True, blank=True, verbose_name=_("Recycled From") ) - name = models.CharField(max_length=200) - shortName = models.CharField(max_length=50) - origin = models.CharField(max_length=50, choices=ORIGINS) - geographicOrigin = models.CharField(max_length=200) - defaultCountry = models.CharField(max_length=3, choices=COUNTRIES) - priority = models.IntegerField() + name = CharField(_("Name"), max_length=200) + shortName = CharField(_("Short Name"), max_length=50) + origin = CharField(_("Origin"), max_length=50, choices=ORIGINS) + geographicOrigin = CharField(_("Geographic Origin"), max_length=200) + defaultCountry = CharField(_("Default Country"), max_length=3, choices=COUNTRIES) + priority = IntegerField() # cff - manufacturerAllocation = models.FloatField(null=True, blank=True) - recycledQualityRatio = models.FloatField(null=True, blank=True) + manufacturerAllocation = FloatField( + _("Manufacturer Allocation"), null=True, blank=True + ) + recycledQualityRatio = FloatField( + _("Recycled Quality Ratio"), null=True, blank=True + ) def __str__(self): return self.name @classmethod - def toJson(cls): + def _fromJSON(cls, material): + """takes a json of a material, returns a Material instance, without recursive FK""" + m = Material( + **delkey( + "recycledFrom", + delkey( + "materialProcessUuid", + delkey( + "recycledProcessUuid", + delkey("primary", flatten("cff", deepcopy(material))), + ), + ), + ) + ) + if material["materialProcessUuid"]: + m.materialProcessUuid = Process.objects.get( + pk=material["materialProcessUuid"] + ) + if material["recycledProcessUuid"]: + m.recycledProcessUuid = Process.objects.get( + pk=material["recycledProcessUuid"] + ) + # if material["recycledFrom"]: + # m.recycledFrom = Material.objects.get(pk=material["recycledFrom"]) + return m + + @classmethod + def allToJSON(cls): return json.dumps( [ { @@ -231,35 +355,71 @@ def toJson(cls): ) -class Example(models.Model): - id = models.CharField(max_length=50, primary_key=True) - name = models.CharField(max_length=200) - category = models.CharField(max_length=50, choices=CATEGORIES) - mass = models.FloatField() - materials = models.ManyToManyField( - Material, through="Share", related_name="examples" +class Example(Model): + id = CharField("ID", max_length=50, primary_key=True) + name = CharField(_("Name"), max_length=200) + + @property + def category(self): + return self.product.name + + mass = FloatField() + materials = ManyToManyField(Material, through="Share") + product = ForeignKey(Product, CASCADE, null=True, verbose_name=_("Category")) + business = CharField(_("Company type"), max_length=50, choices=BUSINESSES) + marketingDuration = FloatField(_("Marketing Duration"), null=True) + numberOfReferences = IntegerField(_("Number Of References"), null=True) + price = FloatField(_("Price"), null=True) + repairCost = FloatField(_("Repair Cost"), null=True, blank=True) + traceability = BooleanField(_("Traceability Displayed?"), null=True) + airTransportRatio = FloatField(_("Air Transport Ratio"), null=True) + + countrySpinning = CharField(_("Spinning Country"), max_length=50, choices=COUNTRIES) + countryFabric = CharField(_("Fabric Country"), max_length=50, choices=COUNTRIES) + countryDyeing = CharField(_("Country Of Dyeing"), max_length=50, choices=COUNTRIES) + countryMaking = CharField( + _("Country Of Manufacture"), max_length=50, choices=COUNTRIES + ) + fabricProcess = ForeignKey( + Process, CASCADE, verbose_name=_("Fabric Process"), null=True ) - product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True) - # fields of products (?) - business = models.CharField(max_length=50, choices=BUSINESSES) - marketingDuration = models.FloatField(null=True) - numberOfReferences = models.IntegerField(null=True) - price = models.FloatField(null=True) - repairCost = models.FloatField(null=True, blank=True) - traceability = models.BooleanField(null=True) - airTransportRatio = models.FloatField(null=True) - - countrySpinning = models.CharField(max_length=50, choices=COUNTRIES) - countryFabric = models.CharField(max_length=50, choices=COUNTRIES) - countryDyeing = models.CharField(max_length=50, choices=COUNTRIES) - countryMaking = models.CharField(max_length=50, choices=COUNTRIES) - fabricProcess = models.ForeignKey(Process, on_delete=models.CASCADE, null=True) def __str__(self): return self.name @classmethod - def toJson(cls): + def _fromJSON(cls, example): + """takes a json of an example, return an instance of Example""" + # all fields except some + e = Example( + **delkey( + "materials", # handled by add_material + delkey( + "category", # computed from product + delkey( + "fabricProcess", # added below + delkey( + "product", # added below + flatten("query", deepcopy(example)), + ), + ), + ), + ) + ) + e.product = Product.objects.get(pk=example["query"]["product"]) + e.fabricProcess = Process.objects.get(alias=example["query"]["fabricProcess"]) + return e + + def add_material(self, share): + """Add a Material to the example""" + Share.objects.create( + material=Material.objects.get(pk=share["id"]), + share=share["share"], + example=self, + ) + + @classmethod + def allToJSON(cls): examples = ( cls.objects.all() .prefetch_related("materials") @@ -311,9 +471,12 @@ def toJson(cls): return json.dumps(output) -class Share(models.Model): +class Share(Model): """m2m relation of Example with an extra field""" - example = models.ForeignKey(Example, on_delete=models.CASCADE) - material = models.ForeignKey(Material, on_delete=models.CASCADE) - share = models.FloatField() + class Meta: + verbose_name_plural = _("Materials") + + example = ForeignKey(Example, CASCADE) + material = ForeignKey(Material, CASCADE) + share = FloatField() diff --git a/backend/update.sh b/backend/update.sh index 8ea54a27e..e8505d027 100755 --- a/backend/update.sh +++ b/backend/update.sh @@ -4,10 +4,10 @@ pushd $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # this script is used at startup on scalingo (see start.sh) # update the l10n and DB -which gettext && django-admin compilemessages # commit mo files -python manage.py makemigrations mailauth authentication #textile +which gettext && django-admin compilemessages +python manage.py makemigrations mailauth authentication #textile # TODO disable textile for now python manage.py migrate # Populate the DB python manage.py shell -c "from authentication.init import init; init()" -#python manage.py shell -c "from textile.init import init; init()" +#python manage.py shell -c "from textile.init import init; init()" # TODO disable textile for now