Django-ViewFlow: How to add CRUD views to flow - django

I've recently come across the Viewflow library for Django which I appears to be a very powerful tool for creating complex workflows.
My app is a simple ticketing system were the workflow is started by creating a Ticket, then a user should be able to create zero or more WorkLog's associated with the ticket via a CRUD page(s), similar to the standard Django admin change_list/detail.
What should the template for the list view look like? I would like to have the UI integrated into the library's frontend.
The flow clearly utilises the following views:
1) CreateView for Ticket
2a) ListView of WorkLog's, template has controls 'back', 'add' (goes to step 2b), 'done' (goes to step 3).
2b) CreateView for WorkLog
3) End
Code:
models.py:
class TicketProcess(Process):
title = models.CharField(max_length=100)
category = models.CharField(max_length=150)
description = models.TextField(max_length=150)
planned = models.BooleanField()
worklogs = models.ForeignKey('WorkLog', null=True)
class WorkLog(models.Model):
ref = models.CharField(max_length=32)
description = models.TextField(max_length=150)
views.py:
class WorkLogListView(FlowListMixin, ListView):
model = WorkLog
class WorkLogCreateView(FlowMixin, CreateView):
model = WorkLog
fields = '__all__'
flows.py:
from .views import WorkLogCreateView
from .models import TicketProcess
#frontend.register
class TicketFlow(Flow):
process_class = TicketProcess
start = (
flow.Start(
CreateProcessView,
fields = ['title', 'category', 'description', 'planned']
).Permission(
auto_create=True
).Next(this.resolution)
)
add_worklog = (
flow.View(
WorkLogListView
).Permission(
auto_create=True
).Next(this.end)
)
end = flow.End()

You can handle that in a different view, or in the same view, just don't call activation.done on a worklog adding request. You can do it by checking what button was pressed in the request.POST data.
#flow.flow_view
def worklog_view(request):
request.activation.prepare(request.POST or None, user=request.user)
if '_logitem' in request.POST:
WorkLog.objects.create(...)
elif request.POST:
activation.done()
request.activation.done()
return redirect(get_next_task_url(request, request.activation.process))
return render(request, 'sometemplate.html', {'activation': request.activation})

Related

Adding User model to the StructBlock class

I'm trying to create a simple blog page with image,content,date posted and user who posted.
But I don't think that wagtail allows me to put in the user model into my block.
class HomePage(Page):
template_name = 'home/home_page.html'
max_count = 1
subtitle = models.CharField(max_length=200)
content = StreamField(
[
("title_and_text_block", AnnouncementBlock())
]
)
content_panels = Page.content_panels + [
FieldPanel("subtitle"),
StreamFieldPanel("content")
]
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, args, kwargs)
context['posts'] = HomePage.objects.live().public()
return context;
from wagtail.core import blocks
from wagtail.images.blocks import ImageChooserBlock
class AnnouncementBlock(blocks.StructBlock):
title = blocks.CharBlock(required=True, help_text='Add your title')
content = blocks.RichTextBlock(required=True, features=["bold", "italic", "link"])
image_thumbnail = ImageChooserBlock(required=False, help_text='Announcement Thumbnail')
class Meta:
template = "streams/title_and_text_block.html"
icon = "edit"
label = "Announcement"
My goal is everytime user posted a new announcement his/her name should appear. not sure how i can do that since in the block i cannot add the User model so that the user's detail will be saved along with the content/image etc.
something like this.
from wagtail.core import blocks
from wagtail.images.blocks import ImageChooserBlock
from django.conf import settings
class AnnouncementBlock(blocks.StructBlock):
title = blocks.CharBlock(required=True, help_text='Add your title')
content = blocks.RichTextBlock(required=True, features=["bold", "italic", "link"])
image_thumbnail = ImageChooserBlock(required=False, help_text='Announcement Thumbnail')
#USER is not allowed here. error on the model
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta:
template = "streams/title_and_text_block.html"
icon = "edit"
label = "Announcement"
Please help thanks
I think you can't use a ForeignKey inside a Streamfield Block class.
If you're OK with displaying a simple user select widget, try to subclass a ChooserBlock (see here).
If you need to assign the logged in user automatically instead, you might be able to write your own block type but 1- it's more complicated as you will have to figure out how Streamfields work internally and 2- if I remember correctly, you can't access the request object from inside a block definition.

2 Users in one model

New to Django. I'm trying to develop a Feedback module, but designing a database structure makes me confused for various reasons:
Where do I need to store feedback_score (positive/neutral/negative feedback ratio), should it be put in custom User, or somewhere else?
How should I get the recipient of the feedback credentials, should it be passed by URL, how to link recipient to FeedbackModel in class-based-views?
How to feedback_score to be calculated each time for every User?
models.py
User = get_user_model()
# Create your models here.
class FeedbackModel(models.Model):
id = models.AutoField(primary_key=False, db_column='id')
sender = models.ForeignKey(
User,
on_delete=models.CASCADE,
primary_key=False, related_name='feedback_left_by')
# recipient = models.ForeignKey(
# User,
# on_delete=models.CASCADE,
# primary_key=True)
FEEDBACK_OPTION = (
(-1, 'Negative'),
(0, 'Neutral'),
(+1, 'Good'),
)
feedback = models.IntegerField(choices=FEEDBACK_OPTION)
opinion = models.CharField(max_length=255)
class Meta:
ordering = ['left_by', '-id']
urls.py
from django.urls import path
from . import views
app_name = 'feedback'
urlpatterns = [
path('leave_feedback/<str:left_to>/',
views.leave_feedback.as_view(), name='leavefeedback'),
]
views.py
from django.shortcuts import render, reverse
from django.views.generic import TemplateView, CreateView
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from .models import FeedbackModel
decorators = [login_required]
#method_decorator(decorators, name='dispatch')
class leave_feedback(CreateView):
model = FeedbackModel
fields = ['feedback', 'opinion']
success_url = '/'
#template_name = "feedback/leave_feedback.html"
def form_valid(self, form):
form.instance.sender = self.request.user
# ?
return super().form_valid(form)
custom user
class User(AbstractUser):
(...)
#property
def feedback_score(self)
return ???
feedback_score should be a class method in FeedbackModel, not a field. In general, you do not want fields in your database that can directly be derived from other fields. For instance:
class Person(models.Model):
name = models.CharField()
birthdate = models.DateField()
zodiac_sign = models.CharField()
If i have the birth date, I can easily get the zodiac sign. So, zodiac_sign should be a method. The same applies to your case. Just create a method that returns summary information on the feedback.
Where do I need to store feedback_score (positive/neutral/negative feedback ratio), should it be put in custom User, or somewhere else?
I think you current Model design is okay with the requirement. Just fix the ordering in class Meta.
BTW, if you want the user to have only one feedback, then consider using OneToOneField. If you want ratio of Feedback, just do like this:
feedbacks = Feeback.objects.all()
positive_feedbacks = feedbacks.filter(feedback=1).count() # returns count of the feedback
negative_feedbacks = feedbacks.filter(feedback=-1).count() # returns count of the feedback
neutral_feedbacks = feedbacks.filter(feedback=0).count() # returns count of the feedback
print("Ratio Positive:Negative:Neutral = {}:{}:{}".format(positive_feedbacks/negative_feedbacks, 1, neutral_feedbacks/negative_feedbacks)
How should I get the recipient of the feedback credentials, should it be passed by URL, how to link recipient to FeedbackModel in class-based-views?
You need to make the user login to the system. For example you can follow this tutorial but there are other good examples out there. Once you login into the system, the user is available via request.user. And your current implement reflects this as well.
How to feedback_score to be calculated each time for every User?
You can get the latest feedback by:
user = User.objects.get(id=1) # or user = request.user
latest_feedback = user.feedback_left_by.last()

django model - fetching user data accross multiple tables

I am writing a django (1.10) website and using allauth for authorisations. I don't want to extend the user model in django - because allauth adds a further layer of complexity to what is already a seemingly convoluted process.
I want to create a model (Custom UserManager?) that will have the following methods:
get_all_subscriptions_for_user(user=specified_user)
get_unexpired_subscriptions_for_user(user=specified_user)
Note: unexpired subscriptions are defined by subscriptions whose end_date > today's date.
This is a snippet of my models.py below
from django.db import models
from django.contrib.auth.models import User
#...
class Subscription(models.Model):
token = models.CharKey()
start_date = models.DateTime()
end_date = models.DateTime()
# other attributes
class UserSubscription(models.Model):
user = models.ForeignKey(User)
subscription = models.ForeignKey(Subscription)
# In view
def foo(request):
user = User.objects.get(username=request.user)
# how can I implement the following methods:
# get_all_subscriptions_for_user(user=specified_user)
# get_unexpired_subscriptions_for_user(user=specified_user)
Ideally, I would like to have a custom user manager, which can fetch this data in one trip to the database - but I'm not sure if I can have a custom user manager without having a custom user model.
[[Aside]]
I'm trying to avoid using a custom model, because it wreaks havoc on the other applications (in my project) which have User as a FK. makemigrations and migrate always barf with a message about inconsistent migration history
You can go with a custom Manager, don't need a UserManager since you are fetching related models:
class UserSubscriptionManager(models.Manager):
def for_user(self, user):
return super(UserSubscriptionManager, self).get_queryset().filter(user=user)
def unexpired_for(self, user):
return self.for_user(user).filter(
suscription__end_date__gt=datetime.date.today() # import datetime
)
in your models:
class UserSubscription(models.Model):
user = models.ForeignKey(User)
subscription = models.ForeignKey(Subscription)
user_objects = UserSubscriptionManager()
this way you can do chain filters in the view, for example:
unexpired_suscriptions = UserSubscription.user_objects().unexpired_for(
user=request.user
).exclude(suscription__token='invalid token')
Try this:
response = []
user_sub = UserSubscription.objects.filter(user=user.pk)
for row in user_sub:
subscription = Subscription.objects.get(pk=row.subscription)
end_date = subscription.end_date
if end_date > timezone.now():
response.append(subscription)

Django admin list_display reverse to parent

I have 2 models:
1: KW (individual keywords)
2: Project (many keywords can belong to many different projects)
class KW(models.Model):
...
project = models.ManyToManyField('KWproject', blank=True)
class KWproject(models.Model):
ProjectKW = models.CharField('Main Keyword', max_length=1000)
author = models.ForeignKey(User, editable=False)
Now when user is in Admin for KWproject they should be able to see all keywords belonging to selected project in list_display. I achieved this but it doesn't feel like proper way.
class ProjectAdmin(admin.ModelAdmin):
form = ProjectForm
list_display = ('Keywordd', 'author')
def Keywordd(self, obj):
return '%s' % (obj.id, obj.ProjectKW)
Keywordd.allow_tags = True
Keywordd.admin_order_field = 'ProjectKW'
Keywordd.short_description = 'ProjectKW'
Is there better way to link and then list_display all items that have reverse relationship to the model? (via "project" field in my example)
As per the Django Admin docs:
ManyToManyField fields aren’t supported, because that would entail
executing a separate SQL statement for each row in the table. If you
want to do this nonetheless, give your model a custom method, and add
that method’s name to list_display. (See below for more on custom
methods in list_display.)
So, you may opt to implement a custom model method like so:
# models.py
class KW(models.Model):
...
project = models.ManyToManyField('KWproject', blank=True)
class KWproject(models.Model):
ProjectKW = models.CharField('Main Keyword', max_length=1000)
author = models.ForeignKey(User, editable=False)
def all_keywords(self):
# Retrieve your keywords
# KW_set here is the default related name. You can set that in your model definitions.
keywords = self.KW_set.values_list('desired_fieldname', flat=True)
# Do some transformation here
desired_output = ','.join(keywords)
# Return value (in example, csv of keywords)
return desired_output
And then, add that model method to your list_display tuple in your ModelAdmin.
# admin.py
class ProjectAdmin(admin.ModelAdmin):
form = ProjectForm
list_display = ('Keywordd', 'author', 'all_keywords')
def Keywordd(self, obj):
return '%s' % (obj.id, obj.ProjectKW)
Keywordd.allow_tags = True
Keywordd.admin_order_field = 'ProjectKW'
Keywordd.short_description = 'ProjectKW'
Do take note: This can potentially be a VERY EXPENSIVE operation. If you are showing 200 rows in the list, then a request to the page will execute 200 additional SQL queries.

Lazy loading a model field's choices

I'm building a Django app to pull in data via an API to track live results of an event with the added ability to override that data before it is displayed.
The first task of the app is to make a request and store the response in the database so I've setup a model;
class ApiData(models.Model):
event = models.CharField(
_("Event"),
max_length=100,
)
key = models.CharField(
_("Data identifier"),
max_length=255,
help_text=_("Something to identify the json stored.")
)
json = JSONField(
load_kwargs={'object_pairs_hook': collections.OrderedDict},
blank=True,
null=True,
)
created = models.DateTimeField()
Ideally I would like it so that objects are created in the admin and the save method populates the ApiData.json field after creating an API request based on the other options in the object.
Because these fields would have choices based on data returned from the API I wanted to lazy load the choices but at the moment I'm just getting a standard Charfield() in my form.
Is this the correct approach for lazy loading model field choices? Or should I just create a custom ModelForm and load the choices there? (That's probably the more typical approach I guess)
def get_event_choices():
events = get_events()
choices = []
for event in events['events']:
choices.append((event['name'], event['title']),)
return choices
class ApiData(models.Model):
# Fields as seen above
def __init__(self, *args, **kwargs):
super(ApiData, self).__init__(*args, **kwargs)
self._meta.get_field_by_name('event')[0]._choices = lazy(
get_event_choices, list
)()
So I went for a typical approach to get this working by simply defining a form for the model admin to use;
# forms.py
from django import forms
from ..models import get_event_choices, ApiData
from ..utils.api import JsonApi
EVENT_CHOICES = get_event_choices()
class ApiDataForm(forms.ModelForm):
"""
Form for collecting the field choices.
The Event field is populated based on the events returned from the API.
"""
event = forms.ChoiceField(choices=EVENT_CHOICES)
class Meta:
model = ApiData
# admin.py
from django.contrib import admin
from .forms.apidata import ApiDataForm
from .models import ApiData
class ApiDataAdmin(admin.ModelAdmin):
form = ApiDataForm
admin.site.register(ApiData, ApiDataAdmin)