Skip to content

Commit

Permalink
Merge pull request #4 from JacobTumak/htmx-dev
Browse files Browse the repository at this point in the history
Demo App htmx dashboard
  • Loading branch information
JacobTumak authored Sep 18, 2024
2 parents b2964ea + 89c1a1b commit 416e293
Show file tree
Hide file tree
Showing 25 changed files with 801 additions and 31 deletions.
2 changes: 1 addition & 1 deletion demo/article/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def new_article_view(request):
draft = form.save(commit=False)
draft.author = user
draft.save()
return redirect("article:detail", draft.id)
else:
form = ArticleForm()
context = {"form": form, "article": Article()}
return render(request, "article/new_article.html", context=context)


@login_required
def edit_article_view(request, article_id):
article = get_object_or_404(Article, id=article_id)
Expand Down
3 changes: 2 additions & 1 deletion demo/assignments/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.contrib import admin

from signoffs.models import Stamp
from signoffs.models import Stamp, ApprovalSignet
from .models import Assignment

admin.site.register(Assignment)
admin.site.register(ApprovalSignet)
admin.site.register(Stamp)
30 changes: 26 additions & 4 deletions demo/assignments/approvals.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
from django.contrib.auth.models import User
from django.utils.functional import SimpleLazyObject

from signoffs.approvals import ApprovalSignoff, SimpleApproval
from signoffs.approvals import ApprovalSignoff, ApprovalRenderer, SimpleApproval
from signoffs.signoffs import SignoffRenderer, SignoffUrlsManager
from signoffs.registry import register
from signoffs.signing_order import SigningOrder


@register("assignments.approvals.NewAssignmentApproval")
class NewAssignmentApproval(SimpleApproval):
S = ApprovalSignoff
render = ApprovalRenderer(approval_template="assignments/htmx_signoffs/approval.html")
label = "Signoff for New Assignment"

S = ApprovalSignoff
S.render = SignoffRenderer(
signoff_form_template="assignments/htmx_signoffs/signoff_form.html",
signet_template="assignments/htmx_signoffs/signet.html",
)
S.urls = SignoffUrlsManager(
save_url_name="assignment:sign-signoff",
revoke_url_name="assignment:revoke-signoff",
)


assign_project_signoff = S.register(
id="assign_project_signoff",
label="Assign Project",
Expand Down Expand Up @@ -40,15 +52,25 @@ class NewAssignmentApproval(SimpleApproval):
confirm_completion_signoff, # completed - unrevokable?
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def next_signoffs(self, for_user=None):
if not for_user:
return super().next_signoffs()
if not type(for_user) in (User, SimpleLazyObject):
raise TypeError(f"var \"for_user\" must be User instance, instead got {type(for_user)}\n")
if not self.subject:

raise ValueError(
f"No Assignment found as subject in {self.id}. Must have subject to check sequential sign perm."
)
assignment = self.subject
if (for_user == assignment.assigned_by and assignment.status in ['draft', 'pending_review']) or (for_user == assignment.assigned_to and assignment.status in ['requested', 'in_progress']):
if (
(for_user == assignment.assigned_by and assignment.status in ['draft', 'pending_review'])
or (for_user == assignment.assigned_to and assignment.status in ['requested', 'in_progress'])
or for_user.is_superuser # FIXME: overwritten for simpler ui testing
):
return super().next_signoffs(for_user=for_user)
else:
return []

211 changes: 211 additions & 0 deletions demo/assignments/htmx_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
from pprint import pprint

from django.contrib.auth.decorators import login_required
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.template.loader import render_to_string
from django.views.decorators.http import require_POST, require_http_methods
from django.contrib import messages
from functools import partial
from icecream import ic

from demo.assignments.approvals import NewAssignmentApproval
from demo.assignments.widget_helpers import (
WIDGET_DIR,
hx_render_approval,
render_new_messages,
render_assignment_selector,
render_assignment_details
)
from signoffs.core.models.fields import ApprovalField
from signoffs.models import ApprovalSignet
from demo.assignments.models import Assignment
from demo.assignments.views import sign_assignment_view
from signoffs.shortcuts import get_approval_or_404


# Change behviour to update individual pieces rather then entire assignment-details. show messages after everything that has messages
# use hx-patch on elements that should be updated?


def get_request(request) -> tuple[WSGIRequest, bool]:
"""Return a tuple `request, is_htmx` where request is the htmx request if `is_htmx`"""
is_htmx = False
if getattr(request, "htmx", False):
htmx_request = request.htmx.request
is_htmx = True
return htmx_request, is_htmx
return request, is_htmx


@login_required
def dashboard_view(request):
assignments = Assignment.objects.all()
ctx = {
"assignments": assignments,
"render_approval": True,
"is_oob": False
}
return render(
request, "assignments/dashboard.html", ctx
)


def update_oob_content(request): # add behaviour to update headers before sending request from the front end
request, _ = get_request(request)
ic(request)
return HttpResponse("Hello")

# def update_from_submission(request):
# html = "\n".join([
# # assignment.approval.render(request_user=request.user, request=request),
# render_assignment_details(request, assignment),
# render_assignment_selector(request, Assignment.objects.all(), notify=True) if signed else "",
# render_new_messages(request)
# ])


def sign_approval_signoff(request):
request, is_htmx = get_request(request)
if not is_htmx:
return sign_assignment_view(request, assignment_pk=request.POST['subject_pk'])

approval = get_approval_or_404(request.POST['approval_type'], pk=request.POST['stamp'])
assignment = get_object_or_404(Assignment, pk=request.POST['subject_pk'])

if assignment.approval_stamp.id != approval.stamp.id:
messages.warning(request, f"🐟 Something smells fishy 🐟")
return HttpResponse(
render_new_messages(request),
headers={"HX-Retarget": "#messages-content"}
)

approval.subject = assignment
form = approval.get_posted_signoff_form(request.POST, request.user)

if not (form and form.is_valid()):
ic(approval.next_signoffs(for_user=request.user))
messages.warning(request, f'You do not have permission to sign this signoff.')

elif not form.is_signed_off():
messages.warning(request, f'You must check the box to sign.')

else:
if signet := form.sign(user=request.user):
assignment.bump_status()
messages.success(request, f'{signet.signoff.id} signed successfully!')
else:
messages.error(request, "Error signing form. Please don't try again later.")

return HttpResponse(
hx_render_approval(approval, request_user=request.user, request=request) +
render_assignment_selector(request, Assignment.objects.all()) +
render_assignment_details(request, assignment, render_approval=False) +
render_new_messages(request)
)


# TODO: use DELETE method
def revoke_signoff(request, signet_pk):
request, is_htmx = get_request(request)

if not is_htmx:
messages.error(request, "This signoff is only revocable in the dashboard")
return redirect(request.META.get("HTTP_REFERER", "assignment:dashboard"))

signet = get_object_or_404(ApprovalSignet, pk=signet_pk)
assignment = get_object_or_404(Assignment, pk=request.POST['subject_pk'])
signet.signoff.revoke_if_permitted(user=request.user)
if not signet.id:
assignment.bump_status(decrease=True)
messages.success(request, "Signoff revoked!")
else:
messages.error(request, "Failed to revoke signoff.")
return HttpResponse(
# request must be supplied to underlying `render_to_string()`,
# otherwise the csrf_token will not be rendered (renders as `none` instead) - I think
hx_render_approval(assignment.approval, request_user=request.user, request=request) +
render_assignment_selector(request, Assignment.objects.all()) +
render_assignment_details(request, assignment, render_approval=False) +
render_new_messages(request)
)


def assignment_details(request, assignment_pk):
request, _ = get_request(request)
assignment = get_object_or_404(Assignment, pk=assignment_pk)
return HttpResponse(
render_assignment_details(request, assignment, is_oob=False) +
render_new_messages(request),
)


def list_assignments(request):
assignments = Assignment.objects.all()
html = "\n".join([
# assignment.approval.render(request_user=request.user, request=request),
render_assignment_details(request, assignments.first()),
render_assignment_selector(request, assignments, is_oob=False),
render_new_messages(request)
])
return HttpResponse(html)


def refresh_messages(request):
new_messages = messages.get_messages(request)
return HttpResponse(render_to_string(WIDGET_DIR/"messages.html", dict(new_messages=new_messages), request))


@require_http_methods(['DELETE'])
def erase_assignment_progress(request, assignment_pk):
request, _ = get_request(request)
assignment = get_object_or_404(Assignment, pk=assignment_pk)
assignment.erase_progress()
assignment.approval_stamp = assignment.approval.stamp
assignment.approval_stamp.save()
messages.success(request, f"Cleared all signoff data for {assignment.assignment_name}")
return HttpResponse(
render_assignment_selector(request, Assignment.objects.all()) +
render_assignment_details(request, assignment, render_approval=True, is_oob=False) +
render_new_messages(request)
)


# @require_POST
# def sign_signoff(request):
# request, is_htmx = get_request(request)
# s_cls = registry.get_signoff_type(request.POST["signoff_id"])
# form = s_cls.forms.get_signoff_form(request.POST)
#
# if form.is_signed_off() and form.is_valid():
# signet = form.sign(request.user)
# if signet:
# messages.success(request, f'{s_cls.id} signed successfully!')
# else:
# messages.error(request, f'Error signing {s_cls.id}')
#
# if approval_id := request.POST.get('approval_id'):
# approval = get_approval_or_404(approval_id, pk=request.POST['stamp'])
# html_content = approval.render(request_user=request.user)
# messages.info(request, f'Updated approval {approval.id}')
# else:
# html_content = signet.signoff.render(request_user=request.user)
#
# if is_htmx:
# html_content += render_to_string(WIDGET_DIR/"messages.html", {}, request)
# return HttpResponse(html_content)


# test buttons


def test_messages(request):
request, _ = get_request(request)

messages.info(request, "Info First")
messages.error(request, "Error Second")
messages.success(request, "Success Third")

messages_html = render_new_messages(request)
return HttpResponse(messages_html)
26 changes: 21 additions & 5 deletions demo/assignments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,32 @@ class Assignment(models.Model):
def assignee(self):
name = self.assigned_to.get_full_name()
if name:
return name
return f"{name} ({self.assigned_to.username})"
else:
return self.assigned_to.username

def bump_status(self, commit=True):
def assigner(self):
name = self.assigned_by.get_full_name()
if name:
return f"{name} ({self.assigned_by.username})"
else:
return self.assigned_by.username

def bump_status(self, commit=True, decrease: bool = False):
direction = -1 if decrease else 1 # [-1, 1][increase] # 1 if increase else - 1
current_index = self.STATUS_OPTS.index([status for status in self.STATUS_OPTS if status[0] == self.status][0])
num_opts = len(self.STATUS_OPTS)
if num_opts <= current_index + 1:
self.status = self.STATUS_OPTS[num_opts - 1][0]
if num_opts - 1 == current_index and not decrease:
self.status = self.STATUS_OPTS[num_opts - 1][0] # Don't go past the last index
else:
self.status = self.STATUS_OPTS[current_index + 1][0]
self.status = self.STATUS_OPTS[current_index + direction][0]
if commit:
self.save()

def erase_progress(self):
self.approval.stamp.delete()
self.approval_stamp = None
self.status = self.STATUS_OPTS[0][0]
self.save()


Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h6 class="text-success">{{ assignment.get_status_display }}</h6>
<br>
<form method="post">
<div class="signoffs approval-signoff">
{% render_approval assignment.approval %}
{% render_approval assignment.approval use_htmx=False %}
</div>
</form>
<br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h1>{{ page_title}} <small>({{ assignments|length }})</small></h1>
<h5 class="card-title">{{ assignment.assignment_name }}</h5>
<h6 class="text-success">{{ assignment.get_status_display }}</h6>
<p class="card-text">Assigned To: {{ assignment.assignee }}</p>
<a href="{% url 'assignment:detail' assignment_id=assignment.id %}" class="btn btn-outline-dark">See Assignment Details</a>
<a href="{% url 'assignment:detail' assignment_pk=assignment.pk %}" class="btn btn-outline-dark">See Assignment Details</a>
</div>
</div>
{% endfor %}
Expand Down
Loading

0 comments on commit 416e293

Please sign in to comment.