diff --git a/uncloud_v3/README-2025.org b/uncloud_v3/README-2025.org new file mode 100644 index 0000000..185e8ab --- /dev/null +++ b/uncloud_v3/README-2025.org @@ -0,0 +1,44 @@ +* Generally to be done for prod [44%] +** TODO Add description to product + - Maybe markdown later +** TODO Link orders to users +** TODO Maybe i18n on everything +** PROGRESS Re-understand get_context_data + - gets additional context such as other models / related models + - Unsure when the context data is being used. + - Usually used in the Detailview of the thing + - so likely referenced by a template or View +** TODO Prevent a product or resource to be named "product" + Because of initial['product'] = self.kwargs['product'] + + + +** DONE Add default values to resources +CLOSED: [2025-03-08 Sat 12:27] +** DONE Re-understand get_form_kwargs +CLOSED: [2025-03-08 Sat 11:48] + - Build the keyword arguments required to instantiate the form. + - Kinda defining the keys needed + + https://docs.djangoproject.com/en/5.1/ref/class-based-views/mixins-editing/ +** DONE Re-understand get_initial +CLOSED: [2025-03-08 Sat 11:48] + - Kinda getting the (initial) values for the keys + - populates form values + " Retrieve initial data for the form. By default, returns a + copy of initial." + https://docs.djangoproject.com/en/5.1/ref/class-based-views/mixins-editing/ + +** DONE Why are (one time) prices separate from resources? +CLOSED: [2025-03-08 Sat 11:31] + A: because duration prices need to be separate. + + - onetime price seems to be reasonable to be inside resource + - Price per timeframe must be separate + - Thus onetime price was also added separately +* Bills [0%] +** TODO Show bills +** TODO Allow to create / send bills +* OIDC integration [%] +** Write code +** Test with authentik diff --git a/uncloud_v3/README.md b/uncloud_v3/README.md index 1dffc98..17a44af 100644 --- a/uncloud_v3/README.md +++ b/uncloud_v3/README.md @@ -64,6 +64,7 @@ machine. Use `kubectl get nodes` to verify minikube is up and running. * resources should have a slug * can be used as an identifier and non unique names * Execute collectstatic for docker +* OIDC / use authentik #### 3.1 (validation release, planned) diff --git a/uncloud_v3/app/forms.py b/uncloud_v3/app/forms.py index 3d9d815..fdfbb89 100644 --- a/uncloud_v3/app/forms.py +++ b/uncloud_v3/app/forms.py @@ -12,7 +12,6 @@ class ProductOneTimeOrderForm(forms.Form): def __init__(self, resources, *args, **kwargs): super().__init__(*args, **kwargs) for res in resources: - print(res) field_name = f"{res.slug}" self.fields[field_name] = forms.FloatField( required=True, diff --git a/uncloud_v3/app/migrations/0006_resource_default_value.py b/uncloud_v3/app/migrations/0006_resource_default_value.py new file mode 100644 index 0000000..f628c66 --- /dev/null +++ b/uncloud_v3/app/migrations/0006_resource_default_value.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2025-03-08 02:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0005_alter_productorder_timeframe_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='resource', + name='default_value', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/uncloud_v3/app/migrations/0007_alter_product_slug_alter_resource_slug.py b/uncloud_v3/app/migrations/0007_alter_product_slug_alter_resource_slug.py new file mode 100644 index 0000000..2ec2b5d --- /dev/null +++ b/uncloud_v3/app/migrations/0007_alter_product_slug_alter_resource_slug.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.2 on 2025-03-08 03:31 + +import app.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0006_resource_default_value'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='slug', + field=models.SlugField(null=True, unique=True, validators=[app.models.validate_name_not_product]), + ), + migrations.AlterField( + model_name='resource', + name='slug', + field=models.SlugField(null=True, unique=True, validators=[app.models.validate_name_not_product]), + ), + ] diff --git a/uncloud_v3/app/models.py b/uncloud_v3/app/models.py index d7bdb07..daf3d63 100644 --- a/uncloud_v3/app/models.py +++ b/uncloud_v3/app/models.py @@ -3,6 +3,20 @@ from django.contrib.auth import get_user_model from django.utils import timezone from django.urls import reverse from django.db.models import Q +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +def validate_name_not_product(value): + """ + We want to prevent overriding our own code. + So the hardcoded name "product" may not be used as a product or resource name + """ + + if value == "product": + raise ValidationError( + _("%(value)s is not allowed as the name"), + params={"value": value}, + ) class Currency(models.Model): slug = models.SlugField(null=True, unique=True) @@ -56,11 +70,12 @@ class PricePerTime(models.Model): return f"{self.value}{self.currency.short_name}/{self.timeframe}" class Resource(models.Model): - slug = models.SlugField(null=True, unique=True) # primary identifier + slug = models.SlugField(null=True, unique=True, validators=[validate_name_not_product]) # primary identifier name = models.CharField(max_length=128, unique=False) # CPU, RAM unit = models.CharField(max_length=128) # Count, GB minimum_units = models.FloatField(null=True, blank=True) # might have min maximum_units = models.FloatField(null=True, blank=True) # might have max + default_value = models.FloatField(null=True, blank=True) # default value to show step_size = models.FloatField(default=1) # step size price_per_time = models.ManyToManyField(PricePerTime, blank=True) @@ -68,6 +83,7 @@ class Resource(models.Model): null=True, blank=True, on_delete=models.CASCADE) + def __str__(self): if self.minimum_units: minimum = self.minimum_units @@ -89,7 +105,7 @@ class Product(models.Model): Describes a product a user can buy """ - slug = models.SlugField(null=True, unique=True) + slug = models.SlugField(null=True, unique=True, validators=[validate_name_not_product]) name = models.CharField(max_length=128, unique=True) resources = models.ManyToManyField(Resource, blank=True) # List of REQUIRED resources diff --git a/uncloud_v3/app/templates/app/productorder_form.html b/uncloud_v3/app/templates/app/productorder_form.html index 8fde29a..84e7420 100644 --- a/uncloud_v3/app/templates/app/productorder_form.html +++ b/uncloud_v3/app/templates/app/productorder_form.html @@ -1,7 +1,7 @@

Order {{ product }}

{% if timeframe %} -

Timeframe: {{ timeframe }}

+

Selected timeframe: {{ timeframe }}

{% endif %}
diff --git a/uncloud_v3/app/views.py b/uncloud_v3/app/views.py index a8cb299..2ee139a 100644 --- a/uncloud_v3/app/views.py +++ b/uncloud_v3/app/views.py @@ -29,11 +29,16 @@ class ProductOneTimeOrderView(FormView): return reverse("order-confirmation") def get_form_kwargs(self): + """ + Keys for the form + """ kwargs = super().get_form_kwargs() # Set the product so the form can retrieve the resources product = get_object_or_404(Product, slug=self.kwargs['product']) kwargs['resources'] = product.resources.all() + + print(f"kwargs = {kwargs}") return kwargs def get_initial(self): @@ -41,10 +46,15 @@ class ProductOneTimeOrderView(FormView): Initial values for the form """ - initial = super().get_initial() + initial = super().get_initial() initial['product'] = self.kwargs['product'] + product = get_object_or_404(Product, slug=self.kwargs['product']) + for res in product.resources.all(): + if res.default_value: + initial[res.slug] = res.default_value + if 'timeframe' in self.kwargs: initial['timeframe'] = self.kwargs['timeframe'] return initial