From e121b135a2bf18b9ccc67f1d7838ed9eabec0448 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 18 Oct 2022 07:22:22 +0100 Subject: [PATCH 1/5] Switch to UWSGI and make docker build/run smarter --- Dockerfile | 7 +++- docker-compose.yml | 32 ++++++++++++++----- .../nginx/conf.d/{default.conf => dev.conf} | 6 ++-- docker/nginx/conf.d/uwsgi.conf | 24 ++++++++++++++ docker/uwsgi.ini | 13 ++++++++ requirements.txt | 1 + 6 files changed, 71 insertions(+), 12 deletions(-) rename docker/nginx/conf.d/{default.conf => dev.conf} (71%) create mode 100644 docker/nginx/conf.d/uwsgi.conf create mode 100644 docker/uwsgi.ini diff --git a/Dockerfile b/Dockerfile index c41a3fb..1f98816 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ # syntax=docker/dockerfile:1 FROM python:3 +ARG OPERATION RUN useradd -d /code xf RUN mkdir -p /code @@ -18,6 +19,10 @@ WORKDIR /code COPY requirements.txt /code/ RUN python -m venv /venv RUN . /venv/bin/activate && pip install -r requirements.txt + # CMD . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini -CMD . /venv/bin/activate && uvicorn --reload --reload-include *.html --workers 2 --uds /var/run/socks/app.sock app.asgi:application + +CMD if [ "$OPERATION" = "uwsgi" ] ; then . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini ; else . /venv/bin/activate && exec python manage.py runserver 0.0.0.0:8000; fi + +# CMD . /venv/bin/activate && uvicorn --reload --reload-include *.html --workers 2 --uds /var/run/socks/app.sock app.asgi:application # CMD . /venv/bin/activate && gunicorn -b 0.0.0.0:8000 --reload app.asgi:application -k uvicorn.workers.UvicornWorker \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 48f53c9..9d6992e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,13 @@ services: app: image: xf/envelope:prod container_name: envelope - build: . + build: + context: . + args: + OPERATION: ${OPERATION} volumes: - ${PORTAINER_GIT_DIR}:/code + - ${PORTAINER_GIT_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini - ${APP_DATABASE_FILE}:/conf/db.sqlite3 - app_static:${STATIC_ROOT} #ports: @@ -22,11 +26,17 @@ services: condition: service_started collectstatic: condition: service_started + networks: + - default + - xf migration: image: xf/envelope:prod container_name: migration_envelope - build: . + build: + context: . + args: + OPERATION: ${OPERATION} command: sh -c '. /venv/bin/activate && python manage.py migrate --noinput' volumes: - ${PORTAINER_GIT_DIR}:/code @@ -38,7 +48,10 @@ services: collectstatic: image: xf/envelope:prod container_name: collectstatic_envelope - build: . + build: + context: . + args: + OPERATION: ${OPERATION} command: sh -c '. /venv/bin/activate && python manage.py collectstatic --noinput' volumes: - ${PORTAINER_GIT_DIR}:/code @@ -59,15 +72,17 @@ services: hard: 65535 volumes: - ${PORTAINER_GIT_DIR}:/code - - ${PORTAINER_GIT_DIR}/docker/nginx/conf.d:/etc/nginx/conf.d + - ${PORTAINER_GIT_DIR}/docker/nginx/conf.d/${OPERATION}.conf:/etc/nginx/conf.d/default.conf - app_static:${STATIC_ROOT} volumes_from: - tmp + networks: + - default + - xf depends_on: app: condition: service_started - # volumes_from: # - tmp # depends_on: @@ -101,9 +116,10 @@ services: # retries: 15 networks: - default: - external: - name: xf + default: + driver: bridge + xf: + external: true volumes: app_static: {} diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/dev.conf similarity index 71% rename from docker/nginx/conf.d/default.conf rename to docker/nginx/conf.d/dev.conf index 9633c7b..bd3ab31 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/dev.conf @@ -1,6 +1,7 @@ upstream django { #server app:8000; - server unix:///var/run/socks/app.sock; + #server unix:///var/run/socks/app.sock; + server app:8000; } server { @@ -9,11 +10,10 @@ server { location = /favicon.ico { access_log off; log_not_found off; } location /static/ { - root /code/core/; + root /conf; } location / { - include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed proxy_pass http://django; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/docker/nginx/conf.d/uwsgi.conf b/docker/nginx/conf.d/uwsgi.conf new file mode 100644 index 0000000..1e19ae9 --- /dev/null +++ b/docker/nginx/conf.d/uwsgi.conf @@ -0,0 +1,24 @@ +upstream django { + server app:8000; + #server unix:///var/run/socks/app.sock; +} + +server { + listen 9999; + + location = /favicon.ico { access_log off; log_not_found off; } + + location /static/ { + root /conf; + } + + location / { + include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed + uwsgi_pass django; + uwsgi_param Host $host; + uwsgi_param X-Real-IP $remote_addr; + uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; + uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto; + } + +} \ No newline at end of file diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini new file mode 100644 index 0000000..d70a784 --- /dev/null +++ b/docker/uwsgi.ini @@ -0,0 +1,13 @@ +[uwsgi] +chdir=/code +module=app.wsgi:application +env=DJANGO_SETTINGS_MODULE=app.settings +master=1 +pidfile=/tmp/project-master.pid +socket=0.0.0.0:8000 +processes=5 +harakiri=20 +max-requests=5000 +vacuum=1 +home=/venv + diff --git a/requirements.txt b/requirements.txt index e7b404f..8b2d48d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ wheel +uwsgi django pre-commit django-crispy-forms From a73f852e39c7197a9327a087474f455508f8e280 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 18 Oct 2022 07:22:22 +0100 Subject: [PATCH 2/5] Tweak UWSGI settings --- docker/uwsgi.ini | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini index d70a784..4f952d0 100644 --- a/docker/uwsgi.ini +++ b/docker/uwsgi.ini @@ -5,9 +5,8 @@ env=DJANGO_SETTINGS_MODULE=app.settings master=1 pidfile=/tmp/project-master.pid socket=0.0.0.0:8000 -processes=5 harakiri=20 -max-requests=5000 +max-requests=100000 vacuum=1 home=/venv - +processes=12 From c54b3e54126dbf8f85dbc33dda23d0e772dea5cc Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 29 Nov 2022 07:20:39 +0000 Subject: [PATCH 3/5] Add local settings to Git --- .gitignore | 1 - app/local_settings.example.py | 35 ----------------------------- app/local_settings.py | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 36 deletions(-) delete mode 100644 app/local_settings.example.py create mode 100644 app/local_settings.py diff --git a/.gitignore b/.gitignore index 5a65867..84f814e 100644 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,6 @@ cover/ # Django stuff: *.log -local_settings.py db.sqlite3 db.sqlite3-journal diff --git a/app/local_settings.example.py b/app/local_settings.example.py deleted file mode 100644 index 66b0677..0000000 --- a/app/local_settings.example.py +++ /dev/null @@ -1,35 +0,0 @@ -# URLs -DOMAIN = "example.com" -URL = f"https://{DOMAIN}" - -# Access control -ALLOWED_HOSTS = ["127.0.0.1", DOMAIN] - -# CSRF -CSRF_TRUSTED_ORIGINS = [URL] - -# Stripe -STRIPE_TEST = True -STRIPE_API_KEY_TEST = "" -STRIPE_PUBLIC_API_KEY_TEST = "" - -STRIPE_API_KEY_PROD = "" -STRIPE_PUBLIC_API_KEY_PROD = "" - -STRIPE_ENDPOINT_SECRET = "" -STATIC_ROOT = "" -SECRET_KEY = "a" - -STRIPE_ADMIN_COUPON = "" - -DEBUG = True -PROFILER = False - -if DEBUG: - import socket # only if you haven't already imported this - - hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) - INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [ - "127.0.0.1", - "10.0.2.2", - ] diff --git a/app/local_settings.py b/app/local_settings.py new file mode 100644 index 0000000..fd418f5 --- /dev/null +++ b/app/local_settings.py @@ -0,0 +1,42 @@ +from os import getenv + +trues = ("t", "true", "yes", "y", "1") + +# URLs +DOMAIN = getenv("DOMAIN", "example.com") +URL = getenv("URL", f"https://{DOMAIN}") + +# Access control +ALLOWED_HOSTS = getenv("ALLOWED_HOSTS", f"127.0.0.1,{DOMAIN}").split(",") + +# CSRF +CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",") + +# Stripe +STRIPE_ENABLED = getenv("STRIPE_ENABLED", "false").lower() in trues +STRIPE_TEST = getenv("STRIPE_TEST", "true") in trues +STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "") +STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "") + +STRIPE_API_KEY_PROD = getenv("STRIPE_API_KEY_PROD", "") +STRIPE_PUBLIC_API_KEY_PROD = getenv("STRIPE_PUBLIC_API_KEY_PROD", "") + +STRIPE_ENDPOINT_SECRET = getenv("STRIPE_ENDPOINT_SECRET", "") +STATIC_ROOT = getenv("STATIC_ROOT", "") +SECRET_KEY = getenv("SECRET_KEY", "") + +STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "") + +DEBUG = getenv("DEBUG", "false") in trues +PROFILER = getenv("PROFILER", "false") in trues + +if DEBUG: + import socket # only if you haven't already imported this + + hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) + INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [ + "127.0.0.1", + "10.0.2.2", + ] + +SETTINGS_EXPORT = ["STRIPE_ENABLED"] \ No newline at end of file From 84be2a72784ad161b992a1afe2be63f1bd98b262 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 29 Nov 2022 07:20:39 +0000 Subject: [PATCH 4/5] Remove async and update CRUD helpers --- app/local_settings.py | 2 +- core/lib/products.py | 6 +- .../templates/wm/{magnet.html => window.html} | 0 core/views/__init__.py | 120 +++++++++++++++--- core/views/base.py | 21 ++- core/views/callbacks.py | 1 - core/views/demo.py | 6 +- 7 files changed, 117 insertions(+), 39 deletions(-) rename core/templates/wm/{magnet.html => window.html} (100%) diff --git a/app/local_settings.py b/app/local_settings.py index fd418f5..d1ccb1d 100644 --- a/app/local_settings.py +++ b/app/local_settings.py @@ -39,4 +39,4 @@ if DEBUG: "10.0.2.2", ] -SETTINGS_EXPORT = ["STRIPE_ENABLED"] \ No newline at end of file +SETTINGS_EXPORT = ["STRIPE_ENABLED"] diff --git a/core/lib/products.py b/core/lib/products.py index 9e9e191..f087f8c 100644 --- a/core/lib/products.py +++ b/core/lib/products.py @@ -1,14 +1,12 @@ -from asgiref.sync import sync_to_async - from core.models import Plan -async def assemble_plan_map(product_id_filter=None): +def assemble_plan_map(product_id_filter=None): """ Get all the plans from the database and create an object Stripe wants. """ line_items = [] - for plan in await sync_to_async(list)(Plan.objects.all()): + for plan in Plan.objects.all(): if product_id_filter: if plan.product_id != product_id_filter: continue diff --git a/core/templates/wm/magnet.html b/core/templates/wm/window.html similarity index 100% rename from core/templates/wm/magnet.html rename to core/templates/wm/window.html diff --git a/core/views/__init__.py b/core/views/__init__.py index a2c2fa5..732a099 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -1,5 +1,8 @@ import uuid +from django.core.exceptions import ImproperlyConfigured +from django.core.paginator import Paginator +from django.db.models import QuerySet from django.http import Http404, HttpResponseBadRequest from django.urls import reverse from django.views.generic.detail import DetailView @@ -12,20 +15,81 @@ from core.util import logs log = logs.get_logger(__name__) -class ObjectList(ListView): +class RestrictedViewMixin: + """ + This mixin overrides two helpers in order to pass the user object to the filters. + get_queryset alters the objects returned for list views. + get_form_kwargs passes the request object to the form class. Remaining permissions + checks are in forms.py + """ + + allow_empty = True + queryset = None + model = None + paginate_by = None + paginate_orphans = 0 + context_object_name = None + paginator_class = Paginator + page_kwarg = "page" + ordering = None + + def get_queryset(self): + """ + This function is overriden to filter the objects by the requesting user. + """ + if self.queryset is not None: + queryset = self.queryset + if isinstance(queryset, QuerySet): + # queryset = queryset.all() + queryset = queryset.filter(user=self.request.user) + elif self.model is not None: + queryset = self.model._default_manager.filter(user=self.request.user) + else: + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.queryset, or override " + "%(cls)s.get_queryset()." % {"cls": self.__class__.__name__} + ) + if hasattr(self, "get_ordering"): + ordering = self.get_ordering() + if ordering: + if isinstance(ordering, str): + ordering = (ordering,) + queryset = queryset.order_by(*ordering) + + return queryset + + def get_form_kwargs(self): + """Passes the request object to the form class. + This is necessary to only display members that belong to a given user""" + + kwargs = super().get_form_kwargs() + kwargs["request"] = self.request + return kwargs + + +class ObjectNameMixin(object): + def __init__(self, *args, **kwargs): + self.title_singular = self.model._meta.verbose_name.title() # Hook + self.context_object_name_singular = self.title_singular.lower() # hook + self.title = self.model._meta.verbose_name_plural.title() # Hooks + self.context_object_name = self.title.lower() # hooks + + self.context_object_name = self.context_object_name.replace(" ", "") + self.context_object_name_singular = self.context_object_name_singular.replace( + " ", "" + ) + super().__init__(*args, **kwargs) + + +class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/objects.html" list_template = None - model = None - context_object_name = "objects" - context_object_name_singular = "object" page_title = None page_subtitle = None - title = "Objects" - title_singular = "Object" - list_url_name = None # WARNING: TAKEN FROM locals() list_url_args = ["type"] @@ -36,6 +100,7 @@ class ObjectList(ListView): # copied from BaseListView def get(self, request, *args, **kwargs): + self.request = request self.object_list = self.get_queryset() allow_empty = self.get_allow_empty() @@ -51,6 +116,7 @@ class ObjectList(ListView): for arg in self.list_url_args: list_url_args[arg] = locals()[arg] + orig_type = type if type == "page": type = "modal" @@ -87,17 +153,19 @@ class ObjectList(ListView): # Return partials for HTMX if self.request.htmx: - self.template_name = self.list_template + if orig_type == "page": + self.template_name = self.list_template + else: + context["window_content"] = self.list_template return self.render_to_response(context) -class ObjectCreate(CreateView): +class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/object-form.html" parser_classes = [FormParser] model = None - context_object_name = "objects" submit_url_name = None list_url_name = None @@ -122,8 +190,13 @@ class ObjectCreate(CreateView): response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response + def form_invalid(self, form): + """If the form is invalid, render the invalid form.""" + return self.get(self.request, **self.kwargs, form=form) + def get(self, request, *args, **kwargs): self.request = request + self.kwargs = kwargs type = kwargs.get("type", None) if not type: return HttpResponseBadRequest("No type specified") @@ -144,6 +217,9 @@ class ObjectCreate(CreateView): list_url = reverse(self.list_url_name, kwargs=list_url_args) context = self.get_context_data() + form = kwargs.get("form", None) + if form: + context["form"] = form context["unique"] = unique context["window_content"] = self.window_content context["context_object_name"] = self.context_object_name @@ -151,7 +227,9 @@ class ObjectCreate(CreateView): context["submit_url"] = submit_url context["list_url"] = list_url context["type"] = type - return self.render_to_response(context) + response = self.render_to_response(context) + # response["HX-Trigger"] = f"{self.context_object_name_singular}Event" + return response def post(self, request, *args, **kwargs): self.request = request @@ -159,21 +237,19 @@ class ObjectCreate(CreateView): return super().post(request, *args, **kwargs) -class ObjectRead(DetailView): +class ObjectRead(RestrictedViewMixin, ObjectNameMixin, DetailView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/object.html" model = None - context_object_name = "object" -class ObjectUpdate(UpdateView): +class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/object-form.html" parser_classes = [FormParser] model = None - context_object_name = "objects" submit_url_name = None request = None @@ -193,6 +269,10 @@ class ObjectUpdate(UpdateView): response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response + def form_invalid(self, form): + """If the form is invalid, render the invalid form.""" + return self.get(self.request, **self.kwargs, form=form) + def get(self, request, *args, **kwargs): self.request = request type = kwargs.get("type", None) @@ -211,13 +291,18 @@ class ObjectUpdate(UpdateView): self.object = self.get_object() submit_url = reverse(self.submit_url_name, kwargs={"type": type, "pk": pk}) context = self.get_context_data() + form = kwargs.get("form", None) + if form: + context["form"] = form context["unique"] = unique context["window_content"] = self.window_content context["context_object_name"] = self.context_object_name context["context_object_name_singular"] = self.context_object_name_singular context["submit_url"] = submit_url context["type"] = type - return self.render_to_response(context) + response = self.render_to_response(context) + # response["HX-Trigger"] = f"{self.context_object_name_singular}Event" + return response def post(self, request, *args, **kwargs): self.request = request @@ -225,9 +310,8 @@ class ObjectUpdate(UpdateView): return super().post(request, *args, **kwargs) -class ObjectDelete(DeleteView): +class ObjectDelete(RestrictedViewMixin, ObjectNameMixin, DeleteView): model = None - context_object_name_singular = "object" template_name = "partials/notify.html" # Overriden to prevent success URL from being used diff --git a/core/views/base.py b/core/views/base.py index 71f5ed4..4eb2a56 100644 --- a/core/views/base.py +++ b/core/views/base.py @@ -1,7 +1,6 @@ import logging import stripe -from asgiref.sync import sync_to_async from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.http import JsonResponse @@ -22,24 +21,24 @@ logger = logging.getLogger(__name__) class Home(View): template_name = "index.html" - async def get(self, request): + def get(self, request): return render(request, self.template_name) class Billing(LoginRequiredMixin, View): template_name = "billing.html" - async def get(self, request): + def get(self, request): if not settings.STRIPE_ENABLED: return redirect(reverse("home")) - plans = await sync_to_async(list)(Plan.objects.all()) - user_plans = await sync_to_async(list)(request.user.plans.all()) + plans = Plan.objects.all() + user_plans = request.user.plans.all() context = {"plans": plans, "user_plans": user_plans} return render(request, self.template_name, context) class Order(LoginRequiredMixin, View): - async def get(self, request, plan_name): + def get(self, request, plan_name): if not settings.STRIPE_ENABLED: return redirect(reverse("home")) plan = Plan.objects.get(name=plan_name) @@ -48,16 +47,14 @@ class Order(LoginRequiredMixin, View): "payment_method_types": settings.ALLOWED_PAYMENT_METHODS, "mode": "subscription", "customer": request.user.stripe_id, - "line_items": await assemble_plan_map( - product_id_filter=plan.product_id - ), + "line_items": assemble_plan_map(product_id_filter=plan.product_id), "success_url": request.build_absolute_uri(reverse("success")), "cancel_url": request.build_absolute_uri(reverse("cancel")), } if request.user.is_superuser: cast["discounts"] = [{"coupon": settings.STRIPE_ADMIN_COUPON}] session = stripe.checkout.Session.create(**cast) - await Session.objects.acreate(user=request.user, session=session.id) + Session.objects.create(user=request.user, session=session.id) return redirect(session.url) # return JsonResponse({'id': session.id}) except Exception as e: @@ -66,7 +63,7 @@ class Order(LoginRequiredMixin, View): class Cancel(LoginRequiredMixin, View): - async def get(self, request, plan_name): + def get(self, request, plan_name): if not settings.STRIPE_ENABLED: return redirect(reverse("home")) plan = Plan.objects.get(name=plan_name) @@ -98,7 +95,7 @@ class Signup(CreateView): class Portal(LoginRequiredMixin, View): - async def get(self, request): + def get(self, request): if not settings.STRIPE_ENABLED: return redirect(reverse("home")) session = stripe.billing_portal.Session.create( diff --git a/core/views/callbacks.py b/core/views/callbacks.py index f193561..450561a 100644 --- a/core/views/callbacks.py +++ b/core/views/callbacks.py @@ -16,7 +16,6 @@ logger = logging.getLogger(__name__) class Callback(APIView): parser_classes = [JSONParser] - # TODO: make async @csrf_exempt def post(self, request): payload = request.body diff --git a/core/views/demo.py b/core/views/demo.py index 4840682..cb51657 100644 --- a/core/views/demo.py +++ b/core/views/demo.py @@ -7,14 +7,14 @@ from django.views import View class DemoModal(View): template_name = "modals/modal.html" - async def get(self, request): + def get(self, request): return render(request, self.template_name) class DemoWidget(View): template_name = "widgets/widget.html" - async def get(self, request): + def get(self, request): unique = str(uuid.uuid4())[:8] return render(request, self.template_name, {"unique": unique}) @@ -22,5 +22,5 @@ class DemoWidget(View): class DemoWindow(View): template_name = "windows/window.html" - async def get(self, request): + def get(self, request): return render(request, self.template_name) From b055dc9f77bbdf5e638825242c58d20e9d1acdb3 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 29 Nov 2022 07:20:39 +0000 Subject: [PATCH 5/5] Update templates --- core/templates/wm/modal.html | 3 ++- core/templates/wm/page.html | 6 ++++++ core/templates/wm/panel.html | 4 +--- core/templates/wm/widget.html | 8 ++++---- core/templates/wm/window.html | 2 ++ 5 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 core/templates/wm/page.html diff --git a/core/templates/wm/modal.html b/core/templates/wm/modal.html index c5973af..b8e5614 100644 --- a/core/templates/wm/modal.html +++ b/core/templates/wm/modal.html @@ -12,8 +12,9 @@ \ No newline at end of file diff --git a/core/templates/wm/page.html b/core/templates/wm/page.html new file mode 100644 index 0000000..93ea8c1 --- /dev/null +++ b/core/templates/wm/page.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + + +{% block content %} + {% include window_content %} +{% endblock %} diff --git a/core/templates/wm/panel.html b/core/templates/wm/panel.html index e57d573..b180b38 100644 --- a/core/templates/wm/panel.html +++ b/core/templates/wm/panel.html @@ -3,9 +3,7 @@

{% block close_button %} - + {% include 'partials/close-window.html' %} {% endblock %} {% block heading %} {% endblock %} diff --git a/core/templates/wm/widget.html b/core/templates/wm/widget.html index dafc773..9ef8154 100644 --- a/core/templates/wm/widget.html +++ b/core/templates/wm/widget.html @@ -1,24 +1,24 @@

-
+