Skip to content

Commit

Permalink
Ability to turn applications on/off implemented (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
s-nagaev authored Nov 25, 2023
1 parent c137c2e commit 4453320
Show file tree
Hide file tree
Showing 23 changed files with 1,316 additions and 1,230 deletions.
15 changes: 9 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@ RUN apk update \
WORKDIR /app

ENV VIRTUAL_ENV=/opt/venv
RUN python -m venv $VIRTUAL_ENV
RUN python -m venv "$VIRTUAL_ENV"
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

COPY requirements.txt /app/requirements.txt
RUN pip install --upgrade pip --no-cache-dir
RUN pip install -r requirements.txt

FROM python:3.10-alpine3.18

LABEL org.label-schema.schema-version = "1.0"
LABEL org.label-schema.name = "Stubborn"
LABEL org.label-schema.vendor = "nagaev.sv@gmail.com"
LABEL org.label-schema.vcs-url = "https://github.com/s-nagaev/stubborn"
LABEL org.label-schema.schema-version="1.0"
LABEL org.label-schema.name="Stubborn"
LABEL org.label-schema.vendor="nagaev.sv@gmail.com"
LABEL org.label-schema.vcs-url="https://github.com/s-nagaev/stubborn"
ENV VIRTUAL_ENV=/opt/venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

RUN apk add --no-cache mailcap libpq-dev make
RUN apk update \
&& apk upgrade \
&& apk add --no-cache --no-cache mailcap libpq-dev make

COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /usr/local/bin /usr/local/bin
Expand Down
4 changes: 2 additions & 2 deletions apps/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.db.models import QuerySet

from apps.models import Application, ResourceStub
from apps.services import turn_off_same_resource_stub
from apps.services import turn_off_same_resource


@admin.action(description='Enable / Disable')
Expand All @@ -14,7 +14,7 @@ def change_satus(model_admin: ModelAdmin, request: WSGIRequest, queryset: QueryS
obj.is_enabled = not obj.is_enabled

if obj.is_enabled:
turn_off_same_resource_stub(resource_stub=obj)
turn_off_same_resource(resource=obj)
obj.save()


Expand Down
60 changes: 47 additions & 13 deletions apps/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import Any, Optional, cast
from typing import Any, cast

from django.conf import settings
from django.contrib import admin, messages
Expand All @@ -25,19 +25,23 @@
RelatedCUDManagerMixin,
SaveByCurrentUserMixin,
)
from apps.services import turn_off_same_resource_stub
from apps.services import turn_off_same_resource
from apps.utils import end_of_the_day_today, prettify_data_to_html, prettify_json_html, start_of_the_day_today


@admin.register(models.Application)
class ApplicationAdmin(admin.ModelAdmin):
readonly_fields = ('owner',)
list_display = ('name', 'slug', 'resources_count', 'short_desc')
list_display = ('get_is_enabled', 'name', 'slug', 'resources_count', 'short_desc')
fields = ('name', 'description', 'slug', 'owner')
inlines = [inlines.LogsInline]
change_form_template = 'admin/apps/application/change_form.html'
ordering = ('name',)
actions = (duplicate,)
ordering = (
'-is_enabled',
'name',
)
actions = (change_satus, duplicate)
list_display_links = ('name',)

class Media:
css = {'all': ('admin/css/application.css',)}
Expand All @@ -64,6 +68,11 @@ def save_model(self, request: HttpRequest, obj: models.Application, *args: Any,
obj.owner = cast(User, request.user)
super().save_model(request, obj, *args, **kwargs)

@staticmethod
@admin.display(boolean=True, description='on')
def get_is_enabled(obj: models.ResourceStub) -> bool:
return obj.is_enabled

@staticmethod
@admin.display(description='Resources')
def resources_count(obj: models.Application) -> int:
Expand All @@ -79,7 +88,7 @@ def resources_count(obj: models.Application) -> int:

@staticmethod
@admin.display(description='Description')
def short_desc(obj: models.Application) -> Optional[str]:
def short_desc(obj: models.Application) -> str | None:
"""Return the first 50 symbols of the description.
Args:
Expand Down Expand Up @@ -125,7 +134,7 @@ class RequestStubAdmin(
ordering = ('-created_at',)
actions = (duplicate,)

def response_add(self, request: HttpRequest, obj: models.RequestStub, post_url_continue: Optional[str] = None):
def response_add(self, request: HttpRequest, obj: models.RequestStub, post_url_continue: str = None):
"""Return to the application page after adding.
Args:
Expand All @@ -152,7 +161,15 @@ class ResourceStubAdmin(
):
form = ResourceStubForm
readonly_fields = ('creator',)
list_display = ('is_enabled', 'get_method', 'uri_with_slash', 'response', 'description', 'full_url', 'proxied')
list_display = (
'get_is_enabled',
'get_method',
'uri_with_slash',
'get_response',
'description',
'full_url',
'proxied',
)
no_add_related = ('application',)
no_edit_related = ('application',)
no_delete_related = ('application',)
Expand All @@ -163,24 +180,41 @@ class ResourceStubAdmin(
'-created_at',
)
actions = (change_satus, duplicate)
list_display_links = ('get_method',)
list_display_links = (
'get_method',
'uri_with_slash',
)

class Media:
js = (
'admin/js/resource/responseSwitcher.js',
'admin/js/resource/hooksSwitcher.js',
)

css = {'all': ('admin/css/resource_list_view.css',)}

def save_model(self, request: HttpRequest, obj: models.ResourceStub, *args: Any, **kwargs: Any) -> None:
if not obj.is_enabled:
super().save_model(request, obj, *args, **kwargs)

if turned_off_resource := turn_off_same_resource_stub(resource_stub=obj):
if turned_off_resource := turn_off_same_resource(resource=obj):
admin_url = reverse('admin:apps_resourcestub_change', args=(turned_off_resource.pk,))
msg = mark_safe(f'A same resource stub has been disabled. <a href={admin_url}>Click here to check it.</a>')
messages.info(request=request, message=msg)
super().save_model(request, obj, *args, **kwargs)

@staticmethod
@admin.display(boolean=True, description='on')
def get_is_enabled(obj: models.ResourceStub) -> bool:
return obj.is_enabled

@staticmethod
@admin.display(description='response')
def get_response(obj: models.ResourceStub) -> str:
if obj.proxy_destination_address:
return "From the destination service"
return str(obj.response)

@staticmethod
@admin.display(description='method')
def get_method(obj: models.ResourceStub) -> str:
Expand Down Expand Up @@ -216,7 +250,7 @@ def full_url(obj: models.ResourceStub) -> str:
return mark_safe(f'<a href={url}>{url}</a>')

def response_add(
self, request: HttpRequest, obj: models.ResourceStub, post_url_continue: Optional[str] = None
self, request: HttpRequest, obj: models.ResourceStub, post_url_continue: str = None
) -> HttpResponseRedirect:
"""Return to the application page after adding.
Expand Down Expand Up @@ -286,7 +320,7 @@ def has_headers(obj: models.ResponseStub) -> bool:
"""
return bool(obj.headers)

def response_add(self, request: HttpRequest, obj: models.ResponseStub, post_url_continue: Optional[str] = None):
def response_add(self, request: HttpRequest, obj: models.ResponseStub, post_url_continue: str = None):
"""Return to the application page after adding.
Args:
Expand Down Expand Up @@ -375,7 +409,7 @@ class Media:

def get_queryset(self, request: HttpRequest) -> QuerySet:
params = request.GET.dict()
application_id = cast(Optional[str], params.get('application'))
application_id = cast(str | None, params.get('application'))
if not application_id:
return super().get_queryset(request)

Expand Down
4 changes: 2 additions & 2 deletions apps/filters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Generator, Union
from typing import Any, Generator
from uuid import UUID

from django.contrib.admin import SimpleListFilter
Expand All @@ -17,7 +17,7 @@ def multiselect_value(self) -> set[str]:
return set()
return set(value.split(','))

def choices(self, changelist: Any) -> Generator[dict[str, Union[bool, str]], None, None]:
def choices(self, changelist: Any) -> Generator[dict[str, bool | str], None, None]:
yield {
'selected': len(self.multiselect_value) == 0,
'query_string': changelist.get_query_string(remove=[self.parameter_name]),
Expand Down
8 changes: 4 additions & 4 deletions apps/hooks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from threading import Event
from time import sleep
from typing import Any, Callable, Dict
from typing import Any, Callable

import requests
from django.conf import settings
Expand Down Expand Up @@ -42,14 +42,14 @@ def process_webhook(*, headers, body, uri, method, query_params, **kwargs):
]
REQUEST_STUB_FIELDS = ['headers', 'body', 'method', 'uri', 'format', 'query_params']

process_action: Dict[str, Callable] = {
process_action: dict[str, Callable] = {
enums.Action.WAIT: process_wait,
enums.Action.WEBHOOK: process_webhook,
}


def _get_hook_context(hook: models.ResourceHook, extra_context: Dict) -> Dict:
context: Dict[str, Any] = {}
def _get_hook_context(hook: models.ResourceHook, extra_context: dict) -> dict:
context: dict[str, Any] = {}
for context_field in [*HOOK_FIELDS, *REQUEST_STUB_FIELDS]:
context.setdefault(context_field, None)

Expand Down
14 changes: 14 additions & 0 deletions apps/migrations/0045_merge_20231008_1606.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 3.2.21 on 2023-10-08 16:06

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('apps', '0044_auto_20230930_1116'),
('apps', '0044_resourcestub_inject_stubborn_headers'),
]

operations = [
]
27 changes: 27 additions & 0 deletions apps/migrations/0046_auto_20231008_1606.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 3.2.21 on 2023-10-08 16:06

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('apps', '0045_merge_20231008_1606'),
]

operations = [
migrations.AddField(
model_name='application',
name='is_enabled',
field=models.BooleanField(default=True, verbose_name='Enabled'),
),
migrations.AlterField(
model_name='application',
name='slug',
field=models.SlugField(allow_unicode=True, verbose_name='Slug'),
),
migrations.AddConstraint(
model_name='application',
constraint=models.UniqueConstraint(condition=models.Q(('is_enabled', True)), fields=('slug', 'name'), name='app_unique_enabled_slug'),
),
]
16 changes: 8 additions & 8 deletions apps/mixins.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import copy
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Sequence, TypeVar, cast
from typing import TYPE_CHECKING, Any, Generic, Sequence, TypeVar, cast

from django import forms
from django.contrib.admin import ModelAdmin
Expand Down Expand Up @@ -29,7 +29,7 @@ class ModelAdminTypeClass(ModelAdmin, InlineModelAdmin):
class HideFromAdminIndexMixin(Generic[_ModelT]):
"""Mixin for hiding registered ModelAdmin instance from the admin index."""

def get_model_perms(self, request: HttpRequest) -> Dict[str, bool]:
def get_model_perms(self, request: HttpRequest) -> dict[str, bool]:
"""Return a dict of all perms for this model.
Return empty dict thus hiding the model from admin index.
Expand All @@ -43,7 +43,7 @@ def get_model_perms(self, request: HttpRequest) -> Dict[str, bool]:
class DenyCreateMixin:
"""Mixin for blocking the creation of a new record through the admin site."""

def has_add_permission(self, request: HttpRequest, obj: Optional[Model] = None) -> bool:
def has_add_permission(self, request: HttpRequest, obj: Model = None) -> bool:
"""Check user "add" permission.
Args:
Expand All @@ -59,7 +59,7 @@ def has_add_permission(self, request: HttpRequest, obj: Optional[Model] = None)
class DenyUpdateMixin:
"""Mixin for blocking the update of an existent record through the admin site."""

def has_change_permission(self, request: HttpRequest, obj: Optional[Model] = None) -> bool:
def has_change_permission(self, request: HttpRequest, obj: Model = None) -> bool:
"""Check user "change" permission.
Args:
Expand All @@ -75,7 +75,7 @@ def has_change_permission(self, request: HttpRequest, obj: Optional[Model] = Non
class DenyDeleteMixin:
"""Mixin for blocking the deletion of an existent record through the admin site."""

def has_delete_permission(self, request: HttpRequest, obj: Optional[Model] = None) -> bool:
def has_delete_permission(self, request: HttpRequest, obj: Model = None) -> bool:
"""Check user "delete" permission.
Args:
Expand Down Expand Up @@ -146,7 +146,7 @@ def _remove_cud_links(self, form: forms.ModelForm) -> forms.ModelForm:
return form

def get_form(
self, request: WSGIRequest, obj: Optional[_ModelT] = None, change: bool = False, **kwargs: Any
self, request: WSGIRequest, obj: _ModelT = None, change: bool = False, **kwargs: Any
) -> forms.ModelForm:
"""Remove CUD icons for related object fields in ModelAdmin.
Expand All @@ -162,7 +162,7 @@ def get_form(
self._remove_cud_links(form)
return form

def get_formset(self, request: WSGIRequest, obj: Optional[_ModelT] = None, **kwargs: Any) -> BaseModelFormSet:
def get_formset(self, request: WSGIRequest, obj: _ModelT = None, **kwargs: Any) -> BaseModelFormSet:
"""Remove CUD icons for related object fields in InlineModelAdmin.
Args:
Expand Down Expand Up @@ -191,7 +191,7 @@ def save_model(self, request: HttpRequest, obj: Model, *args: Any, **kwargs: Any


class AddApplicationRelatedObjectMixin(ModelAdminTypeClass):
def formfield_for_dbfield(self, db_field: Field, request: Optional[HttpRequest], **kwargs: Any) -> Optional[Field]:
def formfield_for_dbfield(self, db_field: Field, request: HttpRequest | None, **kwargs: Any) -> Field | None:
"""Hook for specifying the form Field instance for a given database Field instance.
If kwargs are given, they're passed to the form Field's constructor.
Expand Down
13 changes: 10 additions & 3 deletions apps/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def body_rendered(self) -> str:
class Application(BaseStubModel):
description = models.TextField(verbose_name='Description', null=True, blank=True)
name = models.CharField(max_length=50, verbose_name='Name', null=False)
slug = models.SlugField(verbose_name='Slug', allow_unicode=True, null=False, unique=True)
slug = models.SlugField(verbose_name='Slug', allow_unicode=True, null=False, unique=False)
owner = models.ForeignKey(
User,
verbose_name='Application Owner',
Expand All @@ -99,10 +99,16 @@ class Application(BaseStubModel):
on_delete=models.CASCADE,
related_name='applications',
)
is_enabled = models.BooleanField(verbose_name='Enabled', default=True, null=False)

class Meta:
verbose_name = 'application'
verbose_name_plural = 'applications'
constraints = [
models.UniqueConstraint(
fields=['slug', 'name'], condition=models.Q(is_enabled=True), name='app_unique_enabled_slug'
)
]

def __str__(self) -> str:
"""Object's string representation.
Expand Down Expand Up @@ -248,8 +254,9 @@ class Meta:
verbose_name_plural = 'resources'
constraints = [
models.UniqueConstraint(
fields=['slug', 'method', 'tail', 'application'], condition=models.Q(is_enabled=True),
name='unique_enabled_slug'
fields=['slug', 'method', 'tail', 'application'],
condition=models.Q(is_enabled=True),
name='unique_enabled_slug',
)
]

Expand Down
Loading

0 comments on commit 4453320

Please sign in to comment.