I have a Wagtail model that extends the base Page model:
models.py
class EmployeePage(Page):
eid = models.PositiveIntegerField(unique=True)
active = models.BooleanField(blank=True)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
...
content_panels = [
FieldPanel('eid'),
FieldPanel('first_name'),
FieldPanel('last_name'),
]
I am only updating the active field directly to the live model via daily API import script, so I want it excluded from the CMS entirely.
import_script.py
employee = EmployeePage.objects.get(eid=imported_row.eid)
employee.active = imported_row.active
employee.save()
I'm able to exclude the active field from the CMS edit view by not including it in the content_panels above, but this appears to just be cosmetic as a value is still always included in page revisions, which is overriding my imported value. How can I have a field that is excluded from page revisions?
Here's a solution that's kinda hacky, but seems to work. Instead of excluding the field from page revisions, add code to the import script that updates all page revisions.
import_script.py
employee = EmployeePage.objects.get(eid=imported_row.eid)
employee.status = imported_row.status
employee.save()
# Updates all page revisions
revisions = PageRevision.objects.filter(page=employee)
for r in revisions:
r.active = imported_row.active
r.save()
Related
I would like to limit the choices available of a field in the Wagtail-Admin edit view. My Wagtail version is 2.16.3.
In my case, if have a page model, that describes product categories. To each category might exist some tags.
There is another page model, that describes concrete products and always is a subpage of a category page.
A product can now be described with some tags, but only tags, that belong to the product's category make sense, so I would like to restrict the edit handler of the products to these tags.
My models.py looks similar to this:
class ProductTag(ClusterableModel):
name = models.CharField(_("Name"), max_length=50)
category = ParentalKey(
"ProductCategoryIndexPage", related_name="tags", on_delete=models.CASCADE
)
class ProductPage(Page):
parent_page_types = ["products.ProductCategoryIndexPage"]
tags = ParentalManyToManyField("ProductTag", related_name="products")
content_panels = Page.content_panels + [
FieldPanel("tags", widget=widgets.CheckboxSelectMultiple()),
]
class ProductCategoryIndexPage(Page):
subpage_types = ["products.ProductPage"]
content_panels = Page.content_panels + [
MultiFieldPanel(
[InlinePanel("tags", label="Tags")],
heading=_("Produkt Tags"),
),
]
My approach was to create a custom edit handler and inject the wigdet overriding the on_instance_bound method using the correct choices argument for the widget.
class ProductTagEditPanel(FieldPanel):
def on_instance_bound(self):
self.widget = widgets.CheckboxSelectMultiple(
# of_product is a custom manager method, returning only the tags, that belong to the product's category
choices=ProductTag.objects.of_product(self.instance)
)
return super().on_instance_bound()
But somewhere in the form creation process, the choices are overridden again and I cannot find a good location, where to inject the custom query.
Also looking through the wagtail issues I could only find remotely related stuff and I'm wondering, if my plans are to exotic or I'm sitting to long in front of this problem..
I am encountering what seems to me a weird bug when rendering Inline forms on the "Add" view of a ModelAdmin.
Here is a minimum example with Django version 2.2.4.
in models.py:
class MyModel(models.Model):
text = models.CharField(max_length=100)
class RelatedModel(models.Model):
parent = models.ForeignKey(MyModel, null=False, on_delete=models.CASCADE)
number = models.DecimalField(decimal_places=2, max_digits=10, null=False, blank=False)
in admin.py:
class RelatedModelInlineTabular(admin.TabularInline):
model = RelatedModel
show_change_link = False
fields = ("number", )
class TestMyModelCreate(admin.ModelAdmin):
fields = ['text', ]
inlines = [RelatedModelInlineTabular]
admin.site.register(MyModel, TestMyModelCreate)
Steps to replicate
Login to django admin website
open the "add" view for MyModel (i.e. navigate to the list of Models and click on the "Add new" button)
Expected result
The form displays an empty text field. Below that, an Inline form is displayed with 3 empty rows for potential related instances of RelatedModel
Actual result
The Inline form is displayed twice, each instance with its own 3 empty rows, as if I had specified it twice.
I attach a screenshot below of the actual page (Discount is the name of the related Model). I tried and I get the same result with both StackedInline and TabularInline.
Am I making some trivial error here that could explain what's happening? Or is this is a known bug? Thank you in advance to anyone that will help.
I have a very basic Django Model and use it in Django REST Framework.
When using the browsable API tool and requesting an instance of a ressource, I can set the values of the different attributes using the HTML form at the bottom of the page. All attributes have their current value pre-set except for the DateTime fields. Is there a special setting required such that the form fields for DateTime entries field are prefilled with their current value?
Model class:
class C(models.Model):
OrderDate = models.DateTimeField(auto_now=False)
p1 = models.ForeignKey(PA, related_name="cs", on_delete=models.CASCADE, null=True)
p2 = models.ForeignKey(PB, related_name="cs", on_delete=models.DO_NOTHING, null=True)
class Meta:
ordering = ['OrderDate']
View:
class CViewSet(viewsets.ModelViewSet):
queryset = C.objects.all().order_by('-OrderDate')
serializer_class = CSerializer
def get_queryset(self):
cs = C.objects.all()
# Some filters...
return cs
Serializer:
class CSerializer(serializers.HyperlinkedModelSerializer):
p1 = PASerializer(many=False, read_only=True)
p2 = PBSerializer(many=False, read_only=True)
class Meta:
model = C
fields = (
'id',
'OrderDate',
'p1',
'p2',
)
I tried to generalize the code hope the general idea is clear.
I would like that the OrderDate field is prefilled with the current value when the HTML form in the browsable API is shown.
in DateTimeField you have 2 options, add a default value or use auto_now_add.
in your situation you have set auto_now, and according to this question
auto_now - updates the value of field to current time and date every time the Model.save() is called.
auto_now_add - updates the value with the time and date of creation of record.
so you have this two possibilities:
OrderDate = models.DateTimeField(default=datetime.now)
or
OrderDate = models.DateTimeField(auto_now_add=True)
This is work if you want to automatically add the current time when a user submit the form without displaying it in your HTML, if you want to display the current date and time in html field (your Form), then you have to set it in the frontend side using JavaScript and HTML
Use: <input type="datetime-local">
I'm new to django drf and had the same problem, and this solved it. (DATETIME_FORMAT in settings.py)
my guess is because when rendering the html it uses a different format than the one DRF uses
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS':
'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DATETIME_FORMAT' : '%Y-%m-%dT%H:%M',
}
I am planning to create a page within a blog website where it arranges and displays the all blog posts based on page view count. Not sure how to pull it off.
models.py
class BlogPost(Page):
date = models.DateField(verbose_name="Post date")
categories = ParentalManyToManyField("blog.BlogCategory", blank=True)
tags = ClusterTaggableManager(through="blog.BlogPageTag", blank=True)
body = RichTextField(blank=False)
main_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=False,
on_delete=models.SET_NULL,
related_name='+')
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
blogposts = self.get_siblings().live().public().order_by('-first_published_at')
context['blogposts'] = blogposts
return context
content_panels = Page.content_panels + [
FieldPanel('date'),
FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
FieldPanel('tags'),
ImageChooserPanel('main_image'),
FieldPanel('body', classname="full"),
]
As mentioned in another answer, you can add view_count field to your model. Then you can leverage Wagtail's hooks to increment the value in database.
New field in the model:
class BlogPage(Page):
view_count = models.PositiveBigIntegerField(default=0, db_index=True)
Register the before_serve_page hook:
#hooks.register("before_serve_page")
def increment_view_count(page, request, serve_args, serve_kwargs):
if page.specific_class == BlogPost:
BlogPost.objects.filter(pk=page.pk).update(view_count=F('view_count') + 1)
In this approach database takes responsibility to correctly increment view_count so you don't have to worry about locking and incrementing the value yourself.
If you wanted to count views in a slightly more featured way you could use the django-hitcount package.
Your wagtail_hooks.py file would then become:
from hitcount.models import HitCount
from hitcount.views import HitCountMixin
from wagtail.core import hooks
from home.models import BlogPage
#hooks.register("before_serve_page")
def increment_view_count(page, request, serve_args, serve_kwargs):
if page.specific_class == BlogPage:
hit_count = HitCount.objects.get_for_object(page)
hit_count_response = HitCountMixin.hit_count(request, hit_count)
You need to add HitCountMixin to your Page definition, i.e.
from hitcount.models import HitCountMixin
class BlogPage(Page, HitCountMixin):
This allows you to count hits but avoid duplication from the same IP, to reset hits with a management command, and to set the 'active' period for a page.
You will also need to pip install django-hitcount and add it to your INSTALLED_APPS in settings.py.
As a naive solution, you could add a field view_count to your BlogPage model, this would be a IntegerField.
You will need a way to update this value every time the page is served, you can add some logic to the get_context method you have already used. However, the serve method would be more appropriate, be sure to check that the serve is not being called as a preview by checking request.is_preview.
In regards to querying (ordering by this view_count), this can be done by updating your query.
blogposts = self.get_siblings().live().public().order_by('-view_count')
You can make this new field visible, but not easily editable (client side validation only) by adding it via a FieldPanel with a custom widget. Using the Wagtail settings_panels this can be made available in the non content panel.
Example Model
models.py
from django.db import models
from django import forms
from wagtail.core.models import Page
class BlogPage(Page):
# ... other fields
view_count = models.IntegerField(blank=False, default=0)
content_panels = Page.content_panels + [
# ... existing content panels
]
settings_panels = Page.settings_panels + [
FieldPanel(
'view_count',
# show the view count in the settings tab but do not allow it to be edited
# note: can be easily edited by savvy users, but only if they can also access admin
widget=forms.NumberInput(attrs={'disabled': 'disabled', 'readonly': 'readonly'})
)
]
Please note: this implementation does not take into consideration any other case where serve may be called but does not mean 'a unique user saw my blog post'. You may want to investigate a proper Django analytics solution or some kind of integration with client side analytics such as Google Analytics or Heap.
I imported my (PHP) old site's database tables into Django. By default it created a bunch of primary key fields within the model (since most of them were called things like news_id instead of id).
I just renamed all the primary keys to id and removed the fields from the model. The problem then came specifically with my News model. New stuff that I add doesn't appear in the admin. When I remove the following line from my ModelAdmin, they show up:
list_display = ['headline_text', 'news_category', 'date_posted', 'is_sticky']
Specifically, it's the news_category field that causes problems. If I remove it from that list then I see my new objects. Now, when I edit those items directly (hacking the URL with the item ID) they have a valid category, likewise in the database. Here's the model definitions:
class NewsCategory(models.Model):
def __unicode__(self):
return self.cat_name
#news_category_id = models.IntegerField(primary_key=True, editable=False)
cat_name = models.CharField('Category name', max_length=75)
cat_link = models.SlugField('Category name URL slug', max_length=75, blank=True, help_text='Used in URLs, eg spb.com/news/this-is-the-url-slug/ - generated automatically by default')
class Meta:
db_table = u'news_categories'
ordering = ["cat_name"]
verbose_name_plural = "News categories"
class News(models.Model):
def __unicode__(self):
return self.headline_text
#news_id = models.IntegerField(primary_key=True, editable=False)
news_category = models.ForeignKey('NewsCategory')
writer = models.ForeignKey(Writer) # todo - automate
headline_text = models.CharField(max_length=75)
headline_link = models.SlugField('Headline URL slug', max_length=75, blank=True, help_text='Used in URLs, eg spb.com/news/this-is-the-url-slug/ - generated automatically by default')
body = models.TextField()
extra = models.TextField(blank=True)
date_posted = models.DateTimeField(auto_now_add=True)
is_sticky = models.BooleanField('Is this story featured on the homepage?', blank=True)
tags = TaggableManager(blank=True)
class Meta:
db_table = u'news'
verbose_name_plural = "News"
You can see where I've commented out the autogenerated primary key fields.
It seems like somehow Django thinks my new items don't have news_category_ids, but they definitely do. I tried editing an existing piece of news and changing the category and it worked as normal. If I run a search for one of the new items, it doesn't show up, but the bottom of the search says "1 News found", so something is going on.
Any tips gratefully received.
EDIT: here's my ModelAdmin too:
class NewsCategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {"cat_link": ("cat_name",)}
list_display = ['cat_name', '_cat_count']
def _cat_count(self, obj):
return obj.news_set.count()
_cat_count.short_description = "Number of news stories"
class NewsImageInline(admin.TabularInline):
model = NewsImage
extra = 1
class NewsAdmin(admin.ModelAdmin):
prepopulated_fields = {"headline_link": ("headline_text",)}
list_display = ['headline_text', 'news_category', 'date_posted', 'is_sticky'] #breaking line
list_filter = ['news_category', 'date_posted', 'is_sticky']
search_fields = ['headline_text']
inlines = [NewsImageInline]
The answer you are looking for I think would lie in the SQL schema that you altered and not in the django models.
It could probably have something to do with null or blank values in the news_category_id, or news that belongs to a category that doesn't exist in the news_category. Things I'd check:
You have renamed the primary key on the News category from news_category_id to id. Does the foreign key on the News also map to news_category_id and not anything else?
Are all the values captured in the news.news_category also present in news_category.id
Also, as an aside, I don't see any reason why you need to rename the primary keys to id from something that they already are. Just marking them primary_key=True works just fine. Django provides you a convenient alias pk to access a model's integer primary key, irrespective of what the name of the field actually is.