Skip to content

Commit

Permalink
feat!: add type annotations and restructure
Browse files Browse the repository at this point in the history
- re-init project with cookiecutter
- add type annotations
- upgrade docs
- remove form_button, use django-form-button instead
  • Loading branch information
pradishb committed Sep 18, 2024
1 parent f7d8b76 commit 7c822ba
Show file tree
Hide file tree
Showing 18 changed files with 153 additions and 187 deletions.
39 changes: 17 additions & 22 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
name: Upload Python Package

on:
push:
branches:
- master

- release
permissions:
contents: read

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
14 changes: 10 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ venv/
ENV/
env.bak/
venv.bak/
.vscode

# Spyder project settings
.spyderproject
Expand All @@ -127,8 +128,13 @@ dmypy.json

# Pyre type checker
.pyre/
dist

# Custom
dummy
dummyapp
manage.py
# demo project
/demoproject
/manage.py
/core

# project specific
/test.py
/temp
4 changes: 1 addition & 3 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
MIT License

Copyright (c) 2023 Sandbox
Copyright (c) 2024 Pradish Bijukchhe

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include LICENSE
include README.md
80 changes: 38 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,67 @@
# django-form-action

Django action/button with an intermediate page to parse data from a form
Django action with an intermediate page to parse data from a form

## Installation

Just install the pakage from PyPI
You can install the package via pip:

```
pip install django-form-action
```

## Demo

![Step 1](docs/step1.png)
![Step 2](docs/step2.png)
![Step 3](docs/step3.png)

## Usage

Django admin action with form
![Demo Form Action](https://raw.githubusercontent.com/sandbox-pokhara/django-form-action/master/demo/form-action.gif)
Example usage showing an action in UserAdmin which has an intermediate form that parses data on how to perform that action.

```python
from typing import Any

from django.contrib import admin
from django.contrib import messages
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.db.models import QuerySet
from django.forms import CharField
from django.forms import Form
from django.http import HttpRequest

from dummyapp.models import Fruit
from form_action import form_action


class MyForm(Form):
message = CharField()


@form_action(MyForm, description="Do some task")
def my_action(modeladmin, request, queryset, form):
msg = form.cleaned_data["message"]
messages.add_message(request, messages.INFO, f"Got message: {msg}")

from django_form_action import form_action

@admin.register(Fruit)
class MyModelAdmin(admin.ModelAdmin):
actions = [my_action]
```

Or use it as an extra button with form
![Demo Extra Button](https://raw.githubusercontent.com/sandbox-pokhara/django-form-action/master/demo/extra-button.gif)
admin.site.unregister(User)

```python
from django.contrib import admin
from django.forms import CharField
from django.forms import Form
from django.http.response import HttpResponse

from dummyapp.models import Fruit
from form_action.decorators import extra_button
from form_action.mixins import ExtraButtonMixin
class ChangeFirstName(Form):
first_name = CharField()


class MyForm(Form):
message = CharField()
@form_action(ChangeFirstName, "Change selected users' first name")
def change_first_name(
modeladmin: Any,
request: HttpRequest,
queryset: QuerySet[User],
form: ChangeFirstName,
):
queryset.update(first_name=form.cleaned_data["first_name"])
messages.add_message(
request,
messages.INFO,
"Successfully changed the first name of selected users.",
)


@extra_button("Test Button", MyForm)
def test(request, form):
msg = form.cleaned_data["message"]
return HttpResponse(f"Got message: {msg}")
@admin.register(User)
class CustomUserAdmin(UserAdmin):
actions = [change_first_name]

```

@admin.register(Fruit)
class MyModelAdmin(ExtraButtonMixin, admin.ModelAdmin):
extra_buttons = [test]
## License

```
This project is licensed under the terms of the MIT license.
Binary file removed demo/extra-button.gif
Binary file not shown.
Binary file removed demo/form-action.gif
Binary file not shown.
4 changes: 4 additions & 0 deletions django_form_action/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django_form_action.decorators import form_action

__version__ = "2.0.0"
__all__ = ["form_action"]
79 changes: 44 additions & 35 deletions form_action/decorators.py → django_form_action/decorators.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
from functools import wraps
from typing import Any
from typing import Callable
from typing import Type
from typing import TypeVar
from typing import cast

from django.contrib import admin
from django.db.models import QuerySet
from django.forms import Form
from django.http import HttpRequest
from django.http import HttpResponse
from django.template import RequestContext
from django.template import Template

template = Template("""
template = Template(
"""
{% extends "admin/base_site.html" %}
{% load admin_urls static l10n %}
{% block extrastyle %}
Expand Down Expand Up @@ -36,10 +47,17 @@
</form>
</div>
{% endblock %}
""")
"""
)


def render_form(request, form, title, action="", qs=None):
def render_form(
request: HttpRequest,
form: Form,
title: str,
action: str = "",
qs: Any = None,
):
context = {
"site_header": admin.site.site_header,
"site_title": admin.site.site_title,
Expand All @@ -53,48 +71,39 @@ def render_form(request, form, title, action="", qs=None):
return HttpResponse(template.render(context))


def form_action(form, description):
def decorator(func):
def wrapper(modeladmin, request, queryset):
action = request.POST["action"]
F = TypeVar("F", bound=Form)


def form_action(form_cls: Type[F], description: str):
def decorator(
func: Callable[
[Any, HttpRequest, QuerySet[Any], F],
HttpResponse | None,
],
) -> Callable[[Any, HttpRequest, QuerySet[Any]], HttpResponse | None]:
@wraps(func)
def wrapper(
modeladmin: Any, request: HttpRequest, queryset: QuerySet[Any]
) -> HttpResponse | None:
action = cast(str, request.POST["action"])
if request.POST.get("submit") is not None:
my_form = form(request.POST, request.FILES)
my_form = form_cls(request.POST, request.FILES)
if my_form.is_valid():
# sucess
return func(modeladmin, request, queryset, my_form)
# show form with errors
return render_form(request, my_form, description, action, queryset)
return render_form(
request, my_form, description, action, queryset
)
else:
# show an empty form
return render_form(request, form(), description, action, queryset)
return render_form(
request, form_cls(), description, action, queryset
)

wrapper.short_description = description
wrapper.short_description = description # type:ignore
# required because django requires unique name for action names
wrapper.__name__ = func.__name__
return wrapper

return decorator


def extra_button(title, form=None):
def decorator(func):
def wrapper(request):
if form is None:
return func(request)
if request.POST.get("submit") is not None:
my_form = form(request.POST, request.FILES)
if my_form.is_valid():
# success
return func(request, my_form)
# show form with errors
return render_form(request, my_form, title)
else:
# show an empty form
return render_form(request, form(), title)

wrapper.title = title
wrapper.name = func.__name__
wrapper.__name__ = func.__name__
return wrapper

return decorator
Empty file added django_form_action/py.typed
Empty file.
Binary file added docs/step1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/step3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions form_action/__init__.py

This file was deleted.

47 changes: 0 additions & 47 deletions form_action/mixins.py

This file was deleted.

Loading

0 comments on commit 7c822ba

Please sign in to comment.