Upload file at Wagtail bakerydemo - django

I can't make file upload form field work. My references are:
https://github.com/lb-/bakerydemo/blob/stack-overflow/61289214-wagtail-form-file-upload/bakerydemo/base/models.py
https://dev.to/lb/image-uploads-in-wagtail-forms-39pl
The field was added to admin and appear correctly on my site, but when I try to send the form:
if the field is required, the form is reloaded without any error
if the field is not required, the form is sent but the file is not uploaded
What am I doing wrong? I've been working on this for 3 days and can't find any error message pointing what's wrong. When I get some error message it's always too generic.
Please help! :)
My models.py inside bakerydemo/bakerydemo/base
from __future__ import unicode_literals
import json
from os.path import splitext
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.html import format_html
from django.urls import reverse
from django import forms
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
PageChooserPanel,
StreamFieldPanel,
)
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Collection, Page
from wagtail.contrib.forms.forms import FormBuilder
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormSubmission, AbstractFormField, FORM_FIELD_CHOICES
from wagtail.contrib.forms.views import SubmissionsListView
from wagtail.images import get_image_model
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.images.fields import WagtailImageField
from wagtail.search import index
from wagtail.snippets.models import register_snippet
from .blocks import BaseStreamBlock
from django.core.validators import ValidationError
import logging
#register_snippet
class People(index.Indexed, ClusterableModel):
"""
A Django model to store People objects.
It uses the `#register_snippet` decorator to allow it to be accessible
via the Snippets UI (e.g. /admin/snippets/base/people/)
`People` uses the `ClusterableModel`, which allows the relationship with
another model to be stored locally to the 'parent' model (e.g. a PageModel)
until the parent is explicitly saved. This allows the editor to use the
'Preview' button, to preview the content, without saving the relationships
to the database.
https://github.com/wagtail/django-modelcluster
"""
first_name = models.CharField("First name", max_length=254)
last_name = models.CharField("Last name", max_length=254)
job_title = models.CharField("Job title", max_length=254)
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
panels = [
MultiFieldPanel([
FieldRowPanel([
FieldPanel('first_name', classname="col6"),
FieldPanel('last_name', classname="col6"),
])
], "Name"),
FieldPanel('job_title'),
ImageChooserPanel('image')
]
search_fields = [
index.SearchField('first_name'),
index.SearchField('last_name'),
]
#property
def thumb_image(self):
# Returns an empty string if there is no profile pic or the rendition
# file can't be found.
try:
return self.image.get_rendition('fill-50x50').img_tag()
except: # noqa: E722 FIXME: remove bare 'except:'
return ''
def __str__(self):
return '{} {}'.format(self.first_name, self.last_name)
class Meta:
verbose_name = 'Person'
verbose_name_plural = 'People'
#register_snippet
class FooterText(models.Model):
"""
This provides editable text for the site footer. Again it uses the decorator
`register_snippet` to allow it to be accessible via the admin. It is made
accessible on the template via a template tag defined in base/templatetags/
navigation_tags.py
"""
body = RichTextField()
panels = [
FieldPanel('body'),
]
def __str__(self):
return "Footer text"
class Meta:
verbose_name_plural = 'Footer Text'
class StandardPage(Page):
"""
A generic content page. On this demo site we use it for an about page but
it could be used for any type of page content that only needs a title,
image, introduction and body field
"""
introduction = models.TextField(
help_text='Text to describe the page',
blank=True)
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Landscape mode only; horizontal width between 1000px and 3000px.'
)
body = StreamField(
BaseStreamBlock(), verbose_name="Page body", blank=True
)
content_panels = Page.content_panels + [
FieldPanel('introduction', classname="full"),
StreamFieldPanel('body'),
ImageChooserPanel('image'),
]
class GalleryPage(Page):
"""
This is a page to list locations from the selected Collection. We use a Q
object to list any Collection created (/admin/collections/) even if they
contain no items. In this demo we use it for a GalleryPage,
and is intended to show the extensibility of this aspect of Wagtail
"""
introduction = models.TextField(
help_text='Text to describe the page',
blank=True)
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Landscape mode only; horizontal width between 1000px and '
'3000px.'
)
body = StreamField(
BaseStreamBlock(), verbose_name="Page body", blank=True
)
collection = models.ForeignKey(
Collection,
limit_choices_to=~models.Q(name__in=['Root']),
null=True,
blank=True,
on_delete=models.SET_NULL,
help_text='Select the image collection for this gallery.'
)
content_panels = Page.content_panels + [
FieldPanel('introduction', classname="full"),
StreamFieldPanel('body'),
ImageChooserPanel('image'),
FieldPanel('collection'),
]
# Defining what content type can sit under the parent. Since it's a blank
# array no subpage can be added
subpage_types = []
class HomePage(Page):
"""
The Home Page. This looks slightly more complicated than it is. You can
see if you visit your site and edit the homepage that it is split between
a:
- Hero area
- Body area
- A promotional area
- Moveable featured site sections
"""
# Hero section of HomePage
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Homepage image'
)
hero_text = models.CharField(
max_length=255,
help_text='Write an introduction for the bakery'
)
hero_cta = models.CharField(
verbose_name='Hero CTA',
max_length=255,
help_text='Text to display on Call to Action'
)
hero_cta_link = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
verbose_name='Hero CTA link',
help_text='Choose a page to link to for the Call to Action'
)
# Body section of the HomePage
body = StreamField(
BaseStreamBlock(), verbose_name="Home content block", blank=True
)
# Promo section of the HomePage
promo_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Promo image'
)
promo_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
promo_text = RichTextField(
null=True,
blank=True,
help_text='Write some promotional copy'
)
# Featured sections on the HomePage
# You will see on templates/base/home_page.html that these are treated
# in different ways, and displayed in different areas of the page.
# Each list their children items that we access via the children function
# that we define on the individual Page models e.g. BlogIndexPage
featured_section_1_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
featured_section_1 = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='First featured section for the homepage. Will display up to '
'three child items.',
verbose_name='Featured section 1'
)
featured_section_2_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
featured_section_2 = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Second featured section for the homepage. Will display up to '
'three child items.',
verbose_name='Featured section 2'
)
featured_section_3_title = models.CharField(
null=True,
blank=True,
max_length=255,
help_text='Title to display above the promo copy'
)
featured_section_3 = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text='Third featured section for the homepage. Will display up to '
'six child items.',
verbose_name='Featured section 3'
)
content_panels = Page.content_panels + [
MultiFieldPanel([
ImageChooserPanel('image'),
FieldPanel('hero_text', classname="full"),
MultiFieldPanel([
FieldPanel('hero_cta'),
PageChooserPanel('hero_cta_link'),
]),
], heading="Hero section"),
MultiFieldPanel([
ImageChooserPanel('promo_image'),
FieldPanel('promo_title'),
FieldPanel('promo_text'),
], heading="Promo section"),
StreamFieldPanel('body'),
MultiFieldPanel([
MultiFieldPanel([
FieldPanel('featured_section_1_title'),
PageChooserPanel('featured_section_1'),
]),
MultiFieldPanel([
FieldPanel('featured_section_2_title'),
PageChooserPanel('featured_section_2'),
]),
MultiFieldPanel([
FieldPanel('featured_section_3_title'),
PageChooserPanel('featured_section_3'),
]),
], heading="Featured homepage sections", classname="collapsible")
]
def __str__(self):
return self.title
class FormField(AbstractFormField):
logging.warning('FormField')
page = ParentalKey('FormPage', related_name='form_fields', on_delete=models.CASCADE)
field_type = models.CharField(
verbose_name='field type',
max_length=16,
choices=FORM_FIELD_CHOICES + (('fileupload', 'File Upload'),)
)
class CustomFormBuilder(FormBuilder):
logging.warning('CustomFormBuilder')
def create_fileupload_field(self, field, options):
return forms.FileField(**options)
class CustomSubmissionsListView(SubmissionsListView):
"""
further customisation of submission list can be done here
"""
logging.warning('CustomSubmissionsListView')
pass
class CustomFormSubmission(AbstractFormSubmission):
# important - adding this custom model will make existing submissions unavailable
# can be resolved with a custom migration
def get_data(self):
"""
Here we hook in to the data representation that the form submission returns
Note: there is another way to do this with a custom SubmissionsListView
However, this gives a bit more granular control
"""
logging.warning('CustomFormSubmission')
file_form_fields = [
field.clean_name for field in self.page.specific.get_form_fields()
if field.field_type == 'fileupload'
]
data = super().get_data()
for field_name, field_vale in data.items():
if field_name in file_form_fields:
# now we can update the 'representation' of this value
# we could query the FormUploadedFile based on field_vale (pk)
# then return the filename etc.
pass
return data
def upload_validator(value):
raise ValidationError(value)
class FormUploadedFile(models.Model):
logging.warning('FormUploadedFile')
file = models.FileField(upload_to="files/%Y/%m/%d", validators=[upload_validator])
# file = models.FileField(upload_to="files/%Y/%m/%d")
field_name = models.CharField(blank=True, max_length=254)
class FormPage(AbstractEmailForm):
form_builder = CustomFormBuilder
submissions_list_view_class = CustomSubmissionsListView
logging.warning('FormPage')
image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
# body = RichTextField(default="")
# body = StreamField(BaseStreamBlock())
body = StreamField(
BaseStreamBlock(), verbose_name="Page body", blank=True
)
thank_you_text = RichTextField(blank=True)
# Note how we include the FormField object via an InlinePanel using the
# related_name value
content_panels = AbstractEmailForm.content_panels + [
ImageChooserPanel('image'),
StreamFieldPanel('body'),
InlinePanel('form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"),
MultiFieldPanel([
FieldRowPanel([
FieldPanel('from_address', classname="col6"),
FieldPanel('to_address', classname="col6"),
]),
FieldPanel('subject'),
], "Email"),
]
def get_submission_class(self):
"""
Returns submission class.
Important: will make your existing data no longer visible, only needed if you want to customise
the get_data call on the form submission class, but might come in handy if you do it early
You can override this method to provide custom submission class.
Your class must be inherited from AbstractFormSubmission.
"""
# print('get_submission_class')
return CustomFormSubmission
def process_form_submission(self, form):
"""
Accepts form instance with submitted data, user and page.
Creates submission instance.
You can override this method if you want to have custom creation logic.
For example, if you want to save reference to a user.
"""
# print('process_form_submission')
file_form_fields = [field.clean_name for field in self.get_form_fields() if field.field_type == 'fileupload']
for (field_name, field_value) in form.cleaned_data.items():
if field_name in file_form_fields:
uploaded_file = FormUploadedFile.objects.create(
file=field_value,
field_name=field_name
)
# store a reference to the pk (as this can be converted to JSON)
form.cleaned_data[field_name] = uploaded_file.pk
return self.get_submission_class().objects.create(
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
page=self,
)

Related

Wagtail panel for self-referential many to many relationships with a through model

I am busy making something like a knowledge graph in wagtail.
CurriculumContentItem is a node on that graph. It has a many-to-many relationship with itself, and the through model has important fields.
I'm struggling to get this to be usable in the admin page. Please see the inline comments:
class ContentItemOrder(models.Model):
post = models.ForeignKey(
"CurriculumContentItem", on_delete=models.PROTECT, related_name="pre_ordered_content"
)
pre = models.ForeignKey(
"CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
)
hard_requirement = models.BooleanField(default=True)
class CurriculumContentItem(Page):
body = RichTextField(blank=True)
prerequisites = models.ManyToManyField(
"CurriculumContentItem",
related_name="unlocks",
through="ContentItemOrder",
symmetrical=False,
)
content_panels = Page.content_panels + [
# FieldPanel("prerequisites")
# FieldPanel just lets me select CurriculumContentItems, but I need to access fields in the through model
# InlinePanel("prerequisites"),
# This causes a recursion error
FieldPanel('body', classname="full collapsible"),
]
If I wanted to do this in the normal Django admin I would make use of an inlines to specify prerequisites. Something like:
class ContentItemOrderPostAdmin(admin.TabularInline):
model = models.ContentItem.prerequisites.through
fk_name = "post"
class ContentItemOrderPreAdmin(admin.TabularInline):
model = models.ContentItem.unlocks.through
fk_name = "pre"
Is there a similar mechanism in Wagtail?
It looks like I need to create a custom Panel for this.
I'd suggest constructing an InlinePanel pointing to your 'through' model, which means that you're working with a one-to-many relation rather than many-to-many:
class ContentItemOrder(models.Model):
post = ParentalKey(
"CurriculumContentItem", related_name="pre_ordered_content"
)
pre = models.ForeignKey(
"CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
)
hard_requirement = models.BooleanField(default=True)
panels = [
PageChooserPanel('pre'),
FieldPanel('hard_requirement')
]
class CurriculumContentItem(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
InlinePanel("pre_ordered_content"),
FieldPanel('body', classname="full collapsible"),
]
This works:
Make the through model inherit from Orderable
Make use of ParentalKey instead of ForeignKey
Use InlinePanel referring to the related names of the fields in the through models
from modelcluster.fields import ParentalKey
from wagtail.core.models import Page, Orderable
class ContentItemOrder(Orderable): ### 1
post = ParentalKey( ### 2
"CurriculumContentItem", on_delete=models.PROTECT, related_name="pre_ordered_content"
)
pre = ParentalKey( ### 2
"CurriculumContentItem", on_delete=models.PROTECT, related_name="post_ordered_content"
)
hard_requirement = models.BooleanField(default=True)
panels = [
PageChooserPanel('pre'),
PageChooserPanel('post'),
FieldPanel('hard_requirement'),
]
class CurriculumContentItem(Page):
body = RichTextField(blank=True)
prerequisites = models.ManyToManyField(
"CurriculumContentItem",
related_name="unlocks",
through="ContentItemOrder",
symmetrical=False,
)
content_panels = Page.content_panels + [
InlinePanel('pre_ordered_content', label="prerequisites"), ### 3
InlinePanel('post_ordered_content', label="unlocks"), ### 3
FieldPanel('body', classname="full collapsible"),
]
I was worried that there would be 2 PageChooser fields available per inline but wagtail is clever (and magical) enough that it just draws the one we need

How to make Tags and Categories Separate in django wagtail

How to make Tags and Categories Separate... I have a tag for SCRC and a Tag for Libraries, but How can i make it so when I create a blog, and choose each different tag, the posts show up?
here is my models.py file. thank you for any suggestions.
This is my first django wagtail app and i am trying to create a test blog.
from django.db import models
from django.utils.translation import deactivate_all
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import Tag as TaggitTag
from taggit.models import TaggedItemBase
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
PageChooserPanel,
StreamFieldPanel,
)
from wagtail.core.models import Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.snippets.edit_handlers import SnippetChooserPanel
from wagtail.snippets.models import register_snippet
class BlogsPage(Page):
description = models.CharField(max_length=255, blank=True,)
content_panels = Page.content_panels + [FieldPanel("description", classname="full")]
#register_snippet
class BlogsCategory(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True, max_length=80)
panels = [
FieldPanel("name"),
FieldPanel("slug"),
]
def __str__(self):
return self.name
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
#How am i going to filter this categories ()
#register_snippet
class Tag(TaggitTag):
class Meta:
proxy = True
class PostPageBlogsCategory(models.Model):
page = ParentalKey(
"blogs.PostPage", on_delete=models.CASCADE, related_name="categories"
)
blogs_category = models.ForeignKey(
"blogs.BlogsCategory", on_delete=models.CASCADE, related_name="post_pages"
)
panels = [
SnippetChooserPanel("blogs_category"),
]
class Meta:
unique_together = ("page", "blogs_category")
class PostPageTag(TaggedItemBase):
content_object = ParentalKey("PostPage", related_name="post_tags")
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
tags = ClusterTaggableManager(through="blogs.PostPageTag", blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
]
Snippets are just like any model, except it gets put into a different area of the Wagtail CMS.
Here is a basic example of a snippet in code
#register_snippet
class Category(index.Indexed, models.Model):
name = models.CharField(
max_length=100,
null=True,
)
panels = [
FieldPanel('name'),
]
search_fields = [
index.SearchField('name'),
]
def __str__(self):
return self.name
class Meta:
ordering = ['name']
Here is how you would reference a snippet on a Wagtail Page Model
class BlogPost(Page):
category = ParentalManyToManyField(
'Category',
blank=True,
)
content_panels = Page.content_panels + [
MultiFieldPanel([
FieldPanel(
'category',
widget=forms.CheckboxSelectMultiple,
)
],
heading='Category'
),
]
You can also remove the widget=forms.CheckboxSelectMultiple if that isn't something you want this to do.
By creating a snippet, you now have the option to create "Categories" and then have the ability to select them on the Blog Post.
Once you do that, all you have to do is query the category in the BlogPage model on the HTML template and display it or group it however you want to.
I use snippets when I want more flexibility that tags cannot offer.
EDIT
As I was looking through the Wagtail docs I found something that does exactly what I mentioned

How to add custom fields to Historical Records and assign values to them using signals in django-simple-history

Please check Required Json Here
I wanted to add Custom fields to HistoricalRecords with abstract model.I want to display history records with additional field "replaced_with".Each file history should tell by which file it was replaced.
models.py:
class AdditionalFileHistory(models.Model):
replaced_with = models.CharField(
max_length=100,
null=True,
blank=True,
verbose_name=_('replaced by')
)
class Meta:
abstract = True
class ReviewFile(models.Model):
review = models.OneToOneField(
Review,
verbose_name=_('review'),
on_delete=models.CASCADE,
related_name='files'
)
file = models.FileField(
upload_to='documents/%Y/%m/%d',
null=True,
blank=True,
verbose_name=_('file')
)
filename = models.CharField(
max_length=100,
null=True,
blank=True,
verbose_name=_('filename')
)
history = HistoricalRecords(bases=[AdditionalFileHistory, ], cascade_delete_history=True)
class Meta:
verbose_name_plural = 'Files'
def __str__(self):
return f'Review Step {self.file.name}'
signals.py:
#receiver(pre_create_historical_record)
def pre_create_historical_record_callback(sender, **kwargs):
history_instance = kwargs['history_instance']
source_instance = kwargs['instance']
history_instance.replaced_with = source_instance.filename
it was giving current file history,but we need previous file is replaced by current file in history
something like below:

Django clean() method raises 'Bid' object has no attribute 'kwargs'

I'm trying to restrict a bidder that bids on certain project via a form from bidding neither less than the minimum budget that the project publisher has specifies neither greater than the maximum budget. After checking similar problems i found that overriding the clean method is the most suitable, but i'm facing " 'Bid' object has no attribute 'kwargs' " error
This is the clean method:
def clean(self):
if Project.objects.get(pk=self.kwargs['pk']).budget_min >= self.bid_amount or self.bid_amount >= Project.objects.get(pk=self.kwargs['pk']).budget_max:
raise ValidationError('the bid amount should be between the minimum and maximum project budget')
This is my models.py
# projects/models.py
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.core.validators import MaxValueValidator, MinLengthValidator, MinValueValidator
from upload_validator import FileTypeValidator
from .validators import user_directory_path, validate_file_size
from multiselectfield import MultiSelectField
class Project(models.Model):
title = models.CharField(
'Choose a title for your project',
max_length=255,
validators=[
MinLengthValidator(
15, 'Title must be descriptive and greater than 15 characters')
]
)
body = models.TextField('Tell us more about your project',
max_length=3000,
validators=[
MinLengthValidator(
10, 'Must be descriptive and greater than 10 characters')
]
)
upload_file = models.FileField(
upload_to=user_directory_path,
verbose_name="upload file:(Max file size: 2.5 MB)",
validators=[
validate_file_size,
FileTypeValidator(
allowed_types=[
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/pdf',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'image/tiff',
'image/jpeg',
'image/png'
],
allowed_extensions=['.doc', '.docx',
'.jpg', '.jpeg', '.png', '.pdf', '.zip']
)
],
null=True, blank=True
)
POSSIBLE_SKILLS = (
('Bash Scripting', 'Bash Scripting'), ('CSS', 'CSS'), ('Django', 'Django'),
('HTML', 'HTML'), ('JavaScript', 'JavaScript'), ('React', 'React'),
('Linux', 'Linux'), ('MongoDB', 'MongoDB'), ('NoSQL', 'NoSQL'),
('Powershell', 'Powershell'), ('Python', 'Python'),
('Shell Script', 'Shell Script')
)
skills = MultiSelectField(choices=POSSIBLE_SKILLS, default='')
PAYMENT_TYPE = (
('Fixed', 'Fixed'),
('Hourly', 'Hourly'),
)
payments = models.CharField(
'How do you want to pay?',
max_length=20, choices=PAYMENT_TYPE, default="Fixed")
budget_min = models.PositiveIntegerField(
'What is your estimated Minimum budget?',
default=10, validators=[MinValueValidator(1), MaxValueValidator(100000)])
budget_max = models.PositiveIntegerField(
'What is your estimated Maximum budget? :',
default=100, validators=[MinValueValidator(1), MaxValueValidator(100000)])
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# author is a FOREIGN KEY field in Project table, that refers to the PRIMARY KEY in CustomUser table
# the on_delete method used to tell django when deleting the author delete he's projects
author = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE,
)
# Shows up in the admin list
def __str__(self):
return self.title
def get_absolute_url(self):
"""[summary: where to go when a Project object is created.
]
Returns:
[description: this method should appear to return a string that can be used to refer
to the object over HTTP.]
"""
return reverse('project_detail', args=[str(self.id)])
class Bid(models.Model):
bid_amount = models.PositiveIntegerField(
default=10, validators=[MinValueValidator(1), MaxValueValidator(1000000)]
)
delivery_in = models.PositiveIntegerField(
default=1, validators=[MinValueValidator(1), MaxValueValidator(10000)]
)
describe_your_proposal = models.TextField(
max_length=144,
validators=[
MinLengthValidator(
20, "bid must be descriptive and greater than 20 characters")
]
)
STATUS_TYPE = (
('Approved', 'Approved'),
('Closed', 'Closed'),
('Pending', 'Pending'),
)
status = models.CharField(
'How do you want to pay?',
max_length=20, choices=STATUS_TYPE, default="Pending")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# project is a FOREIGN KEY field in Bid table, that refers to
# the PRIMARY KEY in Project table using related name project.bids
# in templates to access bids of the project
# the on_delete method used to tell django when deleting the project delete it's bids
project = models.ForeignKey(
Project,
on_delete=models.CASCADE,
related_name='bids',
)
# author is a FOREIGN KEY field in Bid table, that refers to
# the PRIMARY KEY in CustomUser table
author = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE,
)
#
def clean(self):
if Project.objects.get(pk=self.kwargs['pk']).budget_min >= self.bid_amount or self.bid_amount >= Project.objects.get(pk=self.kwargs['pk']).budget_max:
raise ValidationError('the bid amount should be between the minimum and maximum project budget')
# Shows up in the admin list
def __str__(self):
return self.describe_your_proposal
def get_absolute_url(self):
"""[summary: where to go when a Bid object is created.
]
Returns:
[description: this method should appear to return a string that can be used
to refer to the object over HTTP.]
"""
return reverse('bid_detail', args=[str(self.id)])
If the project referenced in the clean method is the bid.project object, then you can just call it directly.
def clean(self):
if not self.project.budget_min <= self.bid_amount <= self.project.budget_max:
raise ValidationError('The bid amount should be between the minimum and maximum project budget.')
I'd also recommend switching up the check if the bid is in between the min and max values, I think it is easier to read.

Django contenttypes how to get foreign key values

I'm working on project managment tool for improve my django-skills.
I have a problem with contenttypes.
I have next models:
Project
Ticket - has ForeignKey to Project
Discussion - has ForeignKey to Project
Comment - has ForeignKey to Discussion
UserProfile - extension of django User
ProfileWatch - Projects or Discussions which users watch, contenttype use here
ProfileActions - contains users actions (add comment to discussion, start discussion, add ticket) contenttype also use here. Records in this table creating by signals from Ticket, Discussion and Comment
User recive notification about things that he watch(somebody leave new comment or start discussion or add ticket).
In view I get all notification for current user.
I know wich objects(Project, Discussion) have user actions but I don't know which object trigger this UserAction(Ticket, Comment, Discussion).
Ideally in template I need something like this (in brackets fields of ProfileAction model):
13:55 19.11.2012(action_date) admin(profile) add ticket(action_type) deploy this(?) to JustTestProject(content_object)
But now I have this:
13:55 19.11.2012(action_date) admin(profile) add ticket(action_type) to JustTestProject(content_object)
Any ideas about how organize models for store trigger objects?
Thanks for any help
View:
from django.views.generic import ListView
from app.models import ProfileAction
class ActionsList(ListView):
context_object_name = "actions"
template_name = 'index.html'
def get_queryset(self):
profile = self.request.user.get_profile()
where = ['(content_type_id={0} AND object_id={1})'.format(\
x.content_type_id,\
x.object_id\
) for x in profile.profilewatch_set.all()\
]
recent_actions = ProfileAction.objects.extra(
where=[
' OR '.join(where),
'profile_id={0}'.format(profile.pk)
],
order_by=['-action_date']
)
return recent_actions
Models:
#models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
class UserProfile(models.Model):
user = models.OneToOneField(User, verbose_name=_("Django user"))
first_name = models.CharField(_("First name"), blank=True, max_length=64, null=True)
last_name = models.CharField(_("Last name"), blank=True, max_length=64, null=True)
info = models.TextField(_("Additional information"), null=True)
phone = models.CharField(verbose_name=_("Phone"), max_length=15, blank=True, null=True)
class ProfileWatch(models.Model):
profile = models.ForeignKey(to=UserProfile, verbose_name=_(u"User profile"))
start_date = models.DateTimeField(verbose_name=_(u"Start date"), auto_now_add=True)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
class ProfileAction(models.Model):
ACTION_TYPE_CHOICES = (
(0, _(u"Add comment")),
(1, _(u"Add discussion")),
(2, _(u"Add ticket")),
)
profile = models.ForeignKey(to=UserProfile, verbose_name=_(u"User profile"))
action_date = models.DateTimeField(verbose_name=_(u"Start date"), auto_now_add=True)
action_type = models.PositiveSmallIntegerField(
verbose_name=_("Status"),
choices=ACTION_TYPE_CHOICES
)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
class Project(models.Model):
title = models.CharField(_("Project title"), max_length=128)
description = models.TextField(_("Project description"), blank=True, null=True)
members = models.ManyToManyField(UserProfile, through='Participation', verbose_name=_("Members"), blank=True, null=True)
actions = generic.GenericRelation(ProfileAction)
class Ticket(models.Model):
title = models.CharField(_("Title"), max_length=256)
project = models.ForeignKey('Project', verbose_name=_("Project"))
description = models.TextField(_("Ticket description"), blank=True, null=True)
creator = models.ForeignKey(UserProfile, verbose_name=_("Creator"), related_name='created_tickets')
#receiver(post_save, sender=Ticket)
def add_action_for_ticket(sender, instance, created, **kwargs):
if created:
ProfileAction.objects.create(
profile=instance.creator,
action_type=2,
content_object=instance.project
)
class Discussion(models.Model):
project = models.ForeignKey(
to=Project,
verbose_name=_(u"Project"),
)
creator = models.ForeignKey(
to=UserProfile,
verbose_name=_(u"Creator"),
)
updated = models.DateTimeField(
verbose_name=_("Last update"),
auto_now=True
)
title = models.CharField(
verbose_name=_(u"title"),
max_length=120
)
actions = generic.GenericRelation(ProfileAction)
#receiver(post_save, sender=Discussion)
def add_action_for_discussion(sender, instance, created, **kwargs):
if created:
ProfileAction.objects.create(
profile=instance.creator,
action_type=1,
content_object=instance.project
)
class Comment(models.Model):
discussion = models.ForeignKey(
to=Discussion,
verbose_name=_(u"Discussion")
)
creator = models.ForeignKey(
to=UserProfile,
verbose_name=_(u"Creator"),
)
pub_date = models.DateTimeField(
verbose_name=_("Publication date"),
auto_now_add=True
)
text = models.TextField(
verbose_name=_("Comment text")
)
#receiver(post_save, sender=Comment)
def add_action_for_comment(sender, instance, created, **kwargs):
if created:
ProfileAction.objects.create(
profile=instance.creator,
action_type=0,
content_object=instance.discussion
)
I solved the problem. Just add next adtitional fileds to ProfileAction model and change signals handlers.
trigger_content_type = models.ForeignKey(ContentType, related_name='triggers')
trigger_id = models.PositiveIntegerField()
trigger_object = generic.GenericForeignKey('trigger_content_type', 'trigger_id')