Compare commits
No commits in common. "master" and "3.0.0" have entirely different histories.
37 changed files with 113 additions and 1020 deletions
|
@ -17,7 +17,6 @@ Install system dependencies:
|
|||
|
||||
* On Fedora, you will need the following packages: `python3-virtualenv python3-devel openldap-devel gcc chromium`
|
||||
* sudo apt-get install libpq-dev python-dev libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libffi-dev
|
||||
* On Archlinux, [libldap24](https://aur.archlinux.org/packages/libldap24) is needed
|
||||
|
||||
|
||||
NOTE: you will need to configure a LDAP server and credentials for authentication. See `uncloud/settings.py`.
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
|
@ -1,24 +0,0 @@
|
|||
apiVersion: v2
|
||||
name: django
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
|
@ -1,22 +0,0 @@
|
|||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "django.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "django.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "django.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "django.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
|
@ -1,62 +0,0 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "django.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "django.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "django.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "django.labels" -}}
|
||||
helm.sh/chart: {{ include "django.chart" . }}
|
||||
{{ include "django.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "django.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "django.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "django.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "django.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,61 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "django.fullname" . }}
|
||||
labels:
|
||||
{{- include "django.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "django.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "django.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "django.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
|
@ -1,28 +0,0 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "django.fullname" . }}
|
||||
labels:
|
||||
{{- include "django.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "django.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,61 +0,0 @@
|
|||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "django.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "django.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "django.fullname" . }}
|
||||
labels:
|
||||
{{- include "django.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "django.selectorLabels" . | nindent 4 }}
|
|
@ -1,12 +0,0 @@
|
|||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "django.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "django.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "django.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "django.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "django.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
|
@ -1,82 +0,0 @@
|
|||
# Default values for django.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: nginx
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
|
@ -4,7 +4,7 @@
|
|||
#
|
||||
# While trying to install python-ldap
|
||||
|
||||
FROM python:3.11.1-alpine3.17
|
||||
FROM python:3.10.0-alpine3.15
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
IMAGE=ungleich/uncloud
|
||||
|
||||
all: requirements build
|
||||
|
||||
build:
|
||||
sh -c 'docker build -t $(IMAGE):$$(git describe) .'
|
||||
|
||||
pub: build
|
||||
docker push $(IMAGE):$$(git describe)
|
||||
all: requirements
|
||||
|
||||
run: requirements
|
||||
. ./env && python manage.py runserver
|
||||
|
|
|
@ -12,65 +12,9 @@ machine. Use `kubectl get nodes` to verify minikube is up and running.
|
|||
* `SECRET_KEY`
|
||||
* `DEBUG`
|
||||
* `DATABASE`
|
||||
* Should be: POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, POSTRES_PASSWORD
|
||||
|
||||
## Versions
|
||||
|
||||
#### Future (unplanned)
|
||||
|
||||
* When/where to add timeframe constraints
|
||||
* Timeframe slug-or-id
|
||||
* Maybe slug and backlink to avail products
|
||||
* Timeframe in product
|
||||
* Should a product define list of time frames AND resources?
|
||||
* Then can do autoselect on <resources x timeframes> and only show
|
||||
complete ones
|
||||
* resources are also timeframe bound
|
||||
* name != unique (?)
|
||||
* how do we link?
|
||||
* stays towards resource
|
||||
* we only show resources which have price_per_time with one of
|
||||
our timeframes
|
||||
* And we filter out timeframes that don't have all resources
|
||||
* Can we filter drop down in admin?
|
||||
* yes: ModelAdmin.formfield_for_manytomany(db_field, request, **kwargs)¶
|
||||
* resources should have a slug
|
||||
* can be used as an identifier and non unique names
|
||||
* Execute collectstatic for docker
|
||||
|
||||
#### 3.1 (validation release, planned)
|
||||
|
||||
* Ensure that one resource cannot have multiple price_per_timeframe of
|
||||
the same timeframe
|
||||
* Add wireguard config support
|
||||
|
||||
|
||||
#### 3.0.2 (planned)
|
||||
|
||||
* Add basic validation to ordering
|
||||
|
||||
#### 3.0.1 (planned)
|
||||
|
||||
NEXT STEP: CREATE THE ORDER AND RESOURCE ORDER OBJECTS
|
||||
|
||||
* Show products [done]
|
||||
* Link to ProductOrderForm [done]
|
||||
* Find suitable timeframes for a product [done]
|
||||
* Continue to resources / add resources
|
||||
* Need to list resources [done]
|
||||
* Need to create manytomany relations for each resource resoluting
|
||||
in ResourceOrders
|
||||
* Need to pass in the price for the selected timeframe [done]
|
||||
* On submit
|
||||
* Create ProductOrder
|
||||
* Create ResourceOrder(s)
|
||||
|
||||
#### 3.0.0 (2022-01-14)
|
||||
#### 3.0.0
|
||||
|
||||
* Introduce ProductOrderView
|
||||
|
||||
|
||||
## Pre-Production requirements
|
||||
|
||||
* Products need to ensure *all* resources are consistent for different timeframes
|
||||
* Products cannot have same resource linked twice in same timeframe
|
||||
|
|
|
@ -6,7 +6,6 @@ from .models import *
|
|||
for m in [
|
||||
Currency,
|
||||
Order,
|
||||
OneTimePrice,
|
||||
PricePerTime,
|
||||
Product,
|
||||
ProductOrder,
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
from django import forms
|
||||
from django.forms import NumberInput
|
||||
|
||||
|
||||
class ProductOneTimeOrderForm(forms.Form):
|
||||
"""
|
||||
For products that only contain onetimeresoures
|
||||
"""
|
||||
|
||||
product = forms.SlugField(required=True, disabled=True)
|
||||
|
||||
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,
|
||||
label=res.name,
|
||||
min_value=res.minimum_units,
|
||||
max_value=res.maximum_units,
|
||||
widget=NumberInput(attrs={"step": res.step_size}))
|
||||
|
||||
# if res.minimum_units < res.maximum_units:
|
||||
# self.fields[field_name] = forms.FloatField(
|
||||
# required=True,
|
||||
# label=res.name,
|
||||
# min_value=res.minimum_units,
|
||||
# max_value=res.maximum_units,
|
||||
# widget=NumberInput(attrs={"step": res.step_size}))
|
||||
# else:
|
||||
# self.fields[field_name] = forms.FloatField(widget=forms.HiddenInput(attrs={'value': res.minimum_units}))
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
print("Cleaning form myself ...")
|
||||
|
||||
|
||||
class ProductOrderForm(ProductOneTimeOrderForm):
|
||||
"""
|
||||
For recurring products (might also have OneTime items)
|
||||
"""
|
||||
|
||||
timeframe = forms.SlugField(required=False, disabled=True)
|
|
@ -1,110 +0,0 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from app.models import *
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Add test data'
|
||||
|
||||
# def add_arguments(self, parser):
|
||||
#parser.add_argument('--username', type=str, required=True)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Add CHF as currency
|
||||
currency, created = Currency.objects.get_or_create(defaults=
|
||||
{
|
||||
"slug": "CHF",
|
||||
"name": "Swiss Franc",
|
||||
"short_name": "CHF"
|
||||
})
|
||||
|
||||
# Add standard timeframes
|
||||
for timeframe in [ (3600, "1 hour", "1-hour"),
|
||||
(86400, "1 day", "1-day"),
|
||||
(7*86400, "7 days", "7-days"),
|
||||
(30*86400, "30 days", "30-days"),
|
||||
(365*86400, "365 days", "365 days") ]:
|
||||
TimeFrame.objects.get_or_create(slug=timeframe[2],
|
||||
defaults=
|
||||
{
|
||||
"name": timeframe[1],
|
||||
"seconds": timeframe[0]
|
||||
})
|
||||
|
||||
tf_30d = TimeFrame.objects.get(slug='30-days')
|
||||
|
||||
# Add typical prices per timeframe
|
||||
for ppt in [
|
||||
("1-day", 1, currency),
|
||||
("1-day", 2, currency),
|
||||
("30-days", 2, currency), # HDD 100 GB
|
||||
("30-days", 3, currency), # CPU
|
||||
("30-days", 3.5, currency), # SSD Storage
|
||||
("30-days", 4, currency), # RAM
|
||||
("30-days", 10, currency), # Nextcloud
|
||||
("30-days", 15, currency), # Gitea
|
||||
("30-days", 35, currency), # Matrix
|
||||
("30-days", 29, currency),
|
||||
("30-days", 55, currency) ]:
|
||||
tf = TimeFrame.objects.get(slug=ppt[0])
|
||||
|
||||
PricePerTime.objects.get_or_create(timeframe=tf,
|
||||
value=ppt[1],
|
||||
defaults=
|
||||
{
|
||||
"currency": currency
|
||||
})
|
||||
|
||||
# Add typical resources
|
||||
for res in [
|
||||
# slug name description min max step-size price per 30days
|
||||
("cpu-1", "CPU", "Core(s)", None, None, 0.5, 3),
|
||||
("cpu-min-max", "CPU", "Core(s)", 1, 20, 0.5, 3),
|
||||
("ram-1", "RAM", "GB", None, None, 0.5, 4),
|
||||
("ram-min-max", "RAM", "GB", 1, 200, 0.5, 4),
|
||||
("storage-db", "Database-Storage", "GB", 10, None, 10, 3.5),
|
||||
("storage-ssd", "SSD-Storage", "GB", 10, None, 10, 3.5),
|
||||
("storage-hdd", "HDD-Storage", "GB", 100, None, 100, 2),
|
||||
("matrix-maintenance", "Matrix Maintenance Fee", "", 1, 1, None, 35),
|
||||
("nextcloud-maintenance", "Nextcloud Maintenance Fee", "", 1, 1, None, 10),
|
||||
("gitea-maintenance", "Gitea Maintenance Fee", "", 1, 1, None, 15),
|
||||
|
||||
]:
|
||||
this_res, created = Resource.objects.get_or_create(slug=res[0],
|
||||
defaults=
|
||||
{
|
||||
"name": res[1],
|
||||
"unit": res[2],
|
||||
"minimum_units": res[3],
|
||||
"maximum_units": res[4],
|
||||
"step_size": res[5]
|
||||
})
|
||||
# If price is given, assign it
|
||||
if res[6]:
|
||||
ppt = PricePerTime.objects.get(timeframe=tf_30d, value=res[6])
|
||||
this_res.price_per_time.add(ppt)
|
||||
|
||||
|
||||
# Link resources to prices per time frame
|
||||
|
||||
# Link to PPT -- later
|
||||
# for ppt_res in res[5]:
|
||||
# ppt = PricePerTime.objects.get(
|
||||
|
||||
# Add test products
|
||||
for product in [
|
||||
("matrix", "Matrix"),
|
||||
("nextcloud", "Nextcloud"),
|
||||
("gitea", "Gitea") ]:
|
||||
p, created = Product.objects.get_or_create(slug=product[0],
|
||||
defaults = { "name": product[1] })
|
||||
|
||||
for req_res in [ "cpu-min-max",
|
||||
"ram-min-max",
|
||||
"storage-db",
|
||||
"storage-hdd" ]:
|
||||
print(f"Adding {req_res} to {p}")
|
||||
p.resources.add(Resource.objects.get(slug=req_res))
|
||||
|
||||
p.resources.add(Resource.objects.get(slug=f"{product[0]}-maintenance"))
|
||||
# Every test product can be bought for the 30d timeframe
|
||||
p.timeframes.add(tf_30d)
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 4.0 on 2022-01-16 16:44
|
||||
# Generated by Django 4.0 on 2022-01-02 19:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
@ -18,7 +18,6 @@ class Migration(migrations.Migration):
|
|||
name='Currency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(null=True, unique=True)),
|
||||
('name', models.CharField(max_length=128, unique=True)),
|
||||
('short_name', models.CharField(max_length=3, unique=True)),
|
||||
],
|
||||
|
@ -27,7 +26,7 @@ class Migration(migrations.Migration):
|
|||
name='PricePerTime',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.FloatField()),
|
||||
('price', models.FloatField()),
|
||||
('currency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.currency')),
|
||||
],
|
||||
),
|
||||
|
@ -35,7 +34,6 @@ class Migration(migrations.Migration):
|
|||
name='Product',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(null=True, unique=True)),
|
||||
('name', models.CharField(max_length=128, unique=True)),
|
||||
],
|
||||
),
|
||||
|
@ -43,9 +41,8 @@ class Migration(migrations.Migration):
|
|||
name='Resource',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(null=True, unique=True)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('unit', models.CharField(max_length=128)),
|
||||
('name', models.CharField(max_length=128, unique=True)),
|
||||
('unit', models.CharField(max_length=128, unique=True)),
|
||||
('minimum_units', models.FloatField(blank=True, null=True)),
|
||||
('maximum_units', models.FloatField(blank=True, null=True)),
|
||||
('step_size', models.FloatField(default=1)),
|
||||
|
@ -56,7 +53,6 @@ class Migration(migrations.Migration):
|
|||
name='TimeFrame',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(null=True, unique=True)),
|
||||
('name', models.CharField(max_length=128, unique=True)),
|
||||
('seconds', models.IntegerField(blank=True, null=True)),
|
||||
],
|
||||
|
@ -67,6 +63,7 @@ class Migration(migrations.Migration):
|
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.FloatField()),
|
||||
('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.resource')),
|
||||
('timeframe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.timeframe')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
@ -75,18 +72,12 @@ class Migration(migrations.Migration):
|
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.product')),
|
||||
('resources', models.ManyToManyField(to='app.ResourceOrder')),
|
||||
('timeframe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.timeframe')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='resources',
|
||||
field=models.ManyToManyField(blank=True, to='app.Resource'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='timeframes',
|
||||
field=models.ManyToManyField(blank=True, to='app.TimeFrame'),
|
||||
field=models.ManyToManyField(to='app.Resource'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pricepertime',
|
||||
|
@ -104,12 +95,4 @@ class Migration(migrations.Migration):
|
|||
('product', models.ManyToManyField(blank=True, to='app.ProductOrder')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OneTimePrice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.FloatField()),
|
||||
('currency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.currency')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
28
uncloud_v3/app/migrations/0002_auto_20220102_1953.py
Normal file
28
uncloud_v3/app/migrations/0002_auto_20220102_1953.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.0 on 2022-01-02 19:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def gen_timeframes(apps, schema_editor):
|
||||
# We can't import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
TimeFrame = apps.get_model('app', 'TimeFrame')
|
||||
|
||||
for timeframe in [ (3600, "1 hour"),
|
||||
(86400, "1 day"),
|
||||
(7*86400, "7 days"),
|
||||
(30*86400, "30 days"),
|
||||
(365*86400, "365 days") ]:
|
||||
tf = TimeFrame(name=timeframe[1],
|
||||
seconds=timeframe[0])
|
||||
tf.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(gen_timeframes),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.0 on 2022-01-30 09:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='resource',
|
||||
name='onetime_price',
|
||||
field=models.ManyToManyField(blank=True, to='app.OneTimePrice'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.0 on 2022-01-02 20:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0002_auto_20220102_1953'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='resources',
|
||||
field=models.ManyToManyField(blank=True, to='app.Resource'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='resource',
|
||||
name='unit',
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.0 on 2022-01-30 10:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0002_resource_onetime_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='resource',
|
||||
name='onetime_price',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='resource',
|
||||
name='onetime_price',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='app.onetimeprice'),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.0 on 2022-01-30 10:39
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0003_remove_resource_onetime_price_resource_onetime_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='onetimeprice',
|
||||
options={'ordering': ('value',)},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='productorder',
|
||||
name='timeframe',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='app.timeframe'),
|
||||
),
|
||||
]
|
19
uncloud_v3/app/migrations/0004_auto_20220102_2020.py
Normal file
19
uncloud_v3/app/migrations/0004_auto_20220102_2020.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0 on 2022-01-02 20:20
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def gen_currencies(apps, schema_editor):
|
||||
Currency = apps.get_model('app', 'Currency')
|
||||
|
||||
Currency.objects.get_or_create(name="Swiss Franc",
|
||||
short_name="CHF")
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0003_alter_product_resources_alter_resource_unit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(gen_currencies),
|
||||
]
|
|
@ -1,19 +1,16 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
||||
class Currency(models.Model):
|
||||
slug = models.SlugField(null=True, unique=True)
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
short_name = models.CharField(max_length=3, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.short_name})"
|
||||
|
||||
|
||||
class TimeFrame(models.Model):
|
||||
slug = models.SlugField(null=True, unique=True)
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
seconds = models.IntegerField(null=True, blank=True)
|
||||
|
||||
|
@ -37,36 +34,22 @@ class TimeFrame(models.Model):
|
|||
#return "{} ({})".format(self.name, self.secs_to_name(self.seconds))
|
||||
return f"{self.name}"
|
||||
|
||||
class OneTimePrice(models.Model):
|
||||
value = models.FloatField()
|
||||
currency = models.ForeignKey(Currency, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ('value',)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.value} {self.currency.short_name}"
|
||||
|
||||
class PricePerTime(models.Model):
|
||||
timeframe = models.ForeignKey(TimeFrame, on_delete=models.CASCADE)
|
||||
value = models.FloatField()
|
||||
price = models.FloatField()
|
||||
currency = models.ForeignKey(Currency, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.value}{self.currency.short_name}/{self.timeframe}"
|
||||
return f"{self.price} {self.currency.short_name}/{self.timeframe}"
|
||||
|
||||
class Resource(models.Model):
|
||||
slug = models.SlugField(null=True, unique=True) # primary identifier
|
||||
name = models.CharField(max_length=128, unique=False) # CPU, RAM
|
||||
name = models.CharField(max_length=128, unique=True) # 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
|
||||
step_size = models.FloatField(default=1) # step size
|
||||
step_size = models.FloatField(default=1) # might/must step size
|
||||
|
||||
price_per_time = models.ManyToManyField(PricePerTime, blank=True)
|
||||
onetime_price = models.ForeignKey(OneTimePrice,
|
||||
null=True, blank=True,
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
if self.minimum_units:
|
||||
|
@ -78,10 +61,21 @@ class Resource(models.Model):
|
|||
else:
|
||||
maximum = "No maximum"
|
||||
|
||||
pricing = ", ".join([str(x) for x in self.price_per_time.all()])
|
||||
pricing = []
|
||||
for price in self.price_per_time.all():
|
||||
pricing.append(f"{price.price}{price.currency.short_name}/{price.timeframe}")
|
||||
|
||||
#return f"{self.slug}: {minimum}-{maximum} (+/-){self.step_size} {self.unit} ({pricing})"
|
||||
return f"{self.name} ({self.slug})"
|
||||
pricing = ", ".join(pricing)
|
||||
|
||||
return f"{self.name}: {minimum}-{maximum} (+/-){self.step_size} {self.unit} ({pricing})"
|
||||
|
||||
class ResourceOrder(models.Model):
|
||||
"""
|
||||
Resources that have been ordered
|
||||
"""
|
||||
timeframe = models.ForeignKey(TimeFrame, on_delete=models.CASCADE)
|
||||
value = models.FloatField()
|
||||
resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
|
@ -89,78 +83,23 @@ class Product(models.Model):
|
|||
Describes a product a user can buy
|
||||
"""
|
||||
|
||||
slug = models.SlugField(null=True, unique=True)
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
|
||||
resources = models.ManyToManyField(Resource, blank=True) # List of REQUIRED resources
|
||||
timeframes = models.ManyToManyField(TimeFrame, blank=True) # List of POSSIBLE timeframes
|
||||
# textconfig = models.ManyToManyField(ProductTextConfiguration)
|
||||
# textfieldconfig = models.ManyToManyField(ProductTextFieldConfiguration)
|
||||
|
||||
def has_one_time_price(self):
|
||||
has_otp = False
|
||||
|
||||
for res in self.resources.all():
|
||||
if res.onetime_price:
|
||||
has_otp = True
|
||||
break
|
||||
|
||||
return has_otp
|
||||
|
||||
def valid_timeframes(self):
|
||||
"""
|
||||
Return all timeframes that have all resources configured
|
||||
"""
|
||||
|
||||
valid_tf = []
|
||||
|
||||
num_res = self.resources.all().count()
|
||||
|
||||
for tf in self.timeframes.all():
|
||||
# Get all distinct source for this timeframe
|
||||
res = self.resources.filter(price_per_time__timeframe=tf).distinct().count()
|
||||
|
||||
if res == num_res:
|
||||
valid_tf.append(tf)
|
||||
|
||||
return valid_tf
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('product-detail', kwargs={'slug' : self.slug})
|
||||
resources = models.ManyToManyField(Resource, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class ResourceOrder(models.Model):
|
||||
"""
|
||||
Resources that have been ordered
|
||||
|
||||
We need to record the selected value *and* potentially the
|
||||
calculated price
|
||||
|
||||
"""
|
||||
value = models.FloatField()
|
||||
resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.value} x {self.resource}"
|
||||
|
||||
|
||||
class ProductOrder(models.Model):
|
||||
"""
|
||||
Describes a product a user bought
|
||||
"""
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
timeframe = models.ForeignKey(TimeFrame, null=True, blank=True, on_delete=models.CASCADE)
|
||||
resources = models.ManyToManyField(ResourceOrder)
|
||||
|
||||
def __str__(self):
|
||||
if self.timeframe:
|
||||
txt = f"Order {self.id}: {self.product} for {self.timeframe}"
|
||||
else:
|
||||
txt = f"Order {self.id}: {self.product}"
|
||||
|
||||
return txt
|
||||
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, editable=False)
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from .models import *
|
||||
|
||||
def order_product(product, timeframe, formdata):
|
||||
"""
|
||||
Order a product with given parameters
|
||||
"""
|
||||
|
||||
print(formdata)
|
||||
po = ProductOrder(product=product, timeframe=timeframe)
|
||||
po.save()
|
||||
|
||||
for res, value in formdata.items():
|
||||
print(f"{res}={value}")
|
||||
|
||||
# skip fixed fields
|
||||
if res == 'product' or res == 'timeframe':
|
||||
continue
|
||||
|
||||
resource = get_object_or_404(Resource, slug=res)
|
||||
ro = ResourceOrder.objects.create(value=value, resource=resource)
|
||||
po.resources.add(ro)
|
||||
return po
|
||||
|
||||
# Ordering without a timeframe
|
||||
# if not timeframe:
|
||||
# product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
# timeframe = models.ForeignKey(TimeFrame, null=True, on_delete=models.CASCADE)
|
||||
# resources = models.ManyToManyField(ResourceOrder)
|
|
@ -1,12 +0,0 @@
|
|||
<h1>Welcome to uncloud</h1>
|
||||
|
||||
<h2>What is uncloud?</h2>
|
||||
|
||||
At ungleich we have developed uncloud as an order system that allows
|
||||
you to manage all your resources.
|
||||
|
||||
<h2>What can I do with uncloud?</h2>
|
||||
|
||||
<ul>
|
||||
<li>You can <a href="{% url 'products' %}">order products</a></li>
|
||||
</ul>
|
|
@ -1,13 +0,0 @@
|
|||
<h2>Order Confirmation</h2>
|
||||
|
||||
<p>
|
||||
Thank you for the order. The details are below:
|
||||
</p>
|
||||
<div class="order-details">
|
||||
Order ID: {{ product_order.id }}<br/>
|
||||
Product: {{ product_order.product.name }}
|
||||
</div>
|
||||
<br/>
|
||||
<div>
|
||||
<a href="{% url 'index' %}">Go back to the home page</a>
|
||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||
<h1>{{ object.name }}</h1>
|
||||
|
||||
(description to be added here)
|
||||
|
||||
<ul>
|
||||
{% for tf in timeframes %}
|
||||
<li><a href="{% url 'product-order-tf' object.slug tf.slug %}">Buy
|
||||
{{ object.name }}
|
||||
for {{ tf }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if not timeframes %}
|
||||
{% if has_one_time_price %}
|
||||
<li><a href="{% url 'product-order-onetime' object.slug %}">Buy
|
||||
{{ object.name }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
|
@ -1,13 +0,0 @@
|
|||
<h1>Select Product</h1>
|
||||
|
||||
<h2>Product List</h2>
|
||||
<ul>
|
||||
{% for product in object_list %}
|
||||
{% if product.slug %}
|
||||
<li>
|
||||
<a href="{{ product.get_absolute_url }}">{{ product.name }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -1,13 +1,4 @@
|
|||
<h2>Order {{ product }}</h2>
|
||||
|
||||
{% if timeframe %}
|
||||
<p>Timeframe: {{ timeframe }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" >
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
<button type="submit" class="btn btn-primary">Order</button>
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
|
|
@ -1,113 +1,8 @@
|
|||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import render
|
||||
|
||||
from django.views.generic.edit import CreateView, FormView
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.list import ListView
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.edit import CreateView
|
||||
from .models import ProductOrder
|
||||
|
||||
from .models import *
|
||||
from .forms import *
|
||||
from .services import *
|
||||
|
||||
|
||||
class OrderConfirmationView(TemplateView):
|
||||
template_name = 'app/order_confirmation.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
po_id = self.request.session.pop('product_order_id', None)
|
||||
context['product_order'] = get_object_or_404(ProductOrder, id=po_id)
|
||||
return context
|
||||
|
||||
|
||||
class ProductOneTimeOrderView(FormView):
|
||||
form_class = ProductOneTimeOrderForm
|
||||
template_name = 'app/productorder_form.html'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("order-confirmation")
|
||||
|
||||
def get_form_kwargs(self):
|
||||
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()
|
||||
return kwargs
|
||||
|
||||
def get_initial(self):
|
||||
"""
|
||||
Initial values for the form
|
||||
"""
|
||||
|
||||
initial = super().get_initial()
|
||||
|
||||
initial['product'] = self.kwargs['product']
|
||||
|
||||
if 'timeframe' in self.kwargs:
|
||||
initial['timeframe'] = self.kwargs['timeframe']
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['product'] = get_object_or_404(Product, slug=self.kwargs['product'])
|
||||
if 'timeframe' in context:
|
||||
context['timeframe'] = get_object_or_404(TimeFrame, slug=self.kwargs['timeframe'])
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
product = get_object_or_404(Product, slug=form.cleaned_data['product'])
|
||||
|
||||
print("We got a valid form, let's create the order, listing fields:\n------")
|
||||
for f in form.fields:
|
||||
print(f)
|
||||
print(form.cleaned_data)
|
||||
|
||||
if 'timeframe' in form.cleaned_data:
|
||||
timeframe = get_object_or_404(TimeFrame, slug=form.cleaned_data['timeframe'])
|
||||
else:
|
||||
timeframe = None
|
||||
|
||||
po = order_product(product, timeframe, form.cleaned_data)
|
||||
self.request.session['product_order_id'] = po.id
|
||||
return super().form_valid(form)
|
||||
|
||||
class ProductOrderView(ProductOneTimeOrderView):
|
||||
form_class = ProductOrderForm
|
||||
|
||||
class ProductDetailView(DetailView):
|
||||
model = Product
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['productorder'] = reverse('product-order', kwargs={'product': self.object.slug })
|
||||
context['timeframes'] = context['product'].valid_timeframes()
|
||||
context['has_one_time_price'] = context['product'].has_one_time_price()
|
||||
print(context)
|
||||
return context
|
||||
|
||||
|
||||
class ProductListView(ListView):
|
||||
model = Product
|
||||
|
||||
|
||||
class ProductSelectView(CreateView):
|
||||
class ProductOrderView(CreateView):
|
||||
model = ProductOrder
|
||||
fields = ['product' ]
|
||||
|
||||
class IndexView(TemplateView):
|
||||
"""
|
||||
The starting page containing a short intro
|
||||
"""
|
||||
|
||||
template_name = "app/index.html"
|
||||
|
||||
class Yearly(TemplateView):
|
||||
template_name = "app/config_product.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(Yearly, self).get_context_data(**kwargs)
|
||||
context['current_year'] = self.current_year
|
||||
context['current_month'] = self.current_month
|
||||
return context
|
||||
fields = ['product', 'resources' ]
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
set -x
|
||||
|
||||
name=ungleich/uncloud:$(git describe)
|
||||
name=uncloud:$(git describe)
|
||||
docker build -t ${name} .
|
||||
|
||||
# check for args
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Django basics
|
||||
Django==5.0.2
|
||||
Django==4.0
|
||||
djangorestframework
|
||||
django-auth-ldap
|
||||
|
|
|
@ -26,7 +26,7 @@ SECRET_KEY = os.environ['SECRET_KEY'] if 'SECRET_KEY' in os.environ else 'a bad
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
|
|
@ -19,13 +19,5 @@ from app import views as appviews
|
|||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('order', appviews.ProductSelectView.as_view()),
|
||||
path('order/<slug:product>/', appviews.ProductOrderView.as_view(), name='product-order'),
|
||||
path('order/recurring/<slug:product>/<slug:timeframe>/', appviews.ProductOrderView.as_view(), name='product-order-tf'),
|
||||
path('order/onetime/<slug:product>/', appviews.ProductOneTimeOrderView.as_view(), name='product-order-onetime'),
|
||||
path('order-confirmation', appviews.OrderConfirmationView.as_view(), name='order-confirmation'),
|
||||
path('product/', appviews.ProductListView.as_view(), name='products'),
|
||||
path('product/<slug:slug>/', appviews.ProductDetailView.as_view(), name='product-detail'),
|
||||
path('', appviews.IndexView.as_view(), name='index'),
|
||||
|
||||
path('order', appviews.ProductOrderView.as_view())
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue