How to use chart with a Wagtail CMS - python

I need to include custom charts to a streamfield function, as you can find with Tauchart.
But how is it possible to a user to modify the data from the wagtail admin? I have absolutely no idea on how I can do this.
I'm pondering about a form but it's possible to add a form to a streamfield ? I think yes, I can do it with a link to an existing form. But that seems hard to make and not a good idea.
What do you think about?
What is the best practice for you ?
Do you have any examples of Taucharts integration into a CMS?

You could make a custom block inside blocks.py, for example LineChartBlock() that references a template called line_chart.html. You'll need to import your custom block into your models.py file to add it to your StreamField. Then a user on the CMS admin side can enter values and save them as parameters. Which you can then pass into the template as dynamic JavaScript variables, which you can use to create your custom chart. Here's a super rough idea...
So blocks.py would look like this:
from wagtail.core import blocks
class LineChartBlock(blocks.StructBlock):
title = blocks.CharBlock()
x = blocks.DecimalBlock()
y = blocks.DecimalBlock()
data = JSONField()
class Meta:
template = 'blocks/line_chart.html'
Your models.py:
class BlogPage(Page):
body = StreamField([
('paragraph', RichTextBlock()),
('line_chart', LineChartBlock()),
])
Your template:
<script>
const title = "{{ title }}";
const x = "{{ x }}";
const y = "{{ y }}";
const data = "{{ data }}";
makeChartWizardry = () => {
doStuff;
}
</script>
<section>
<div id="chart"></div>
</section>

Related

Django removes my custom html attributes from option tag

I am trying to add custom data-* attributes to the option tag in a select element. I have my custom template (widget) which is used by Django, but it seems like Django removes my custom attributes. somewhere in further steps
My custom option template:
widgets/tree_option_template.html
<option value="{{ widget.attrs.lft|stringformat:'s' }}"
data-test="test"
>{{ widget.label }} - {{ widget.attrs.rght|stringformat:'s' }}</option>
Custom widget:
class MultiChoiceFilterWidget(forms.SelectMultiple):
"""
TODO.
Based on FilteredSelectMultiple
"""
option_inherits_attrs = True
option_template_name = "widgets/tree_option_template.html"
...
Usage in admin.py:
class UserAdminForm(forms.ModelForm):
class Meta:
model = User
fields = "__all__"
read_projects = CustomTreeNodeMultipleChoiceField(
queryset=Project.objects.filter(disabled=False),
required=False,
widget=MultiChoiceFilterWidget(verbose_name="Projects", is_stacked=False),
)
When I am changing e.g. value attribute then it changes in DOM as well but my custom attributes are not available in HTML:
As we can see there is no data-test attribute...
Any idea why my custom tags are not visible in HTML?
Thanks!
ANSWER/SOLUTION
I found the problem. The problem was that the FilteredSelectMultiple after which I inherit has attached some JS files which were manipulating my HTML attributes.
#property
def media(self):
extra = "" if settings.DEBUG else ".min"
js = [
"vendor/jquery/jquery%s.js" % extra,
"jquery.init.js",
"core.js",
"SelectBox.js",
"SelectFilter2.js",
]
...
When I removed these JS files (SelectBox.js precisely) then my custom HTML attributes appeared.

How do I count hits of each element in a list in Django?

So I have a page where multiple articles are listed. (To be precise, TITLES that are outlinked to the articles written on Notion template.) And I want to have a filed in my model that counts the number of clicks of each article. (I don't want to use django-hitcount library).
Let me first show you my code. models.py
class Article(models.Model):
number = models.IntegerField(primary_key=True)
title = models.CharField(max_length=20, default="")
url = models.URLField(max_length=100, default="")
hits = models.IntegerField(default=0)
template
...
<div class="col text-center">
{% for q in allArticles %}
<h2 id={{q.number}}>{{q.title}}</h2>
{% endfor %}
</div>
...
I was thinking of using onclick() event in JavaScript, but then passing data from JavaScript to Django seemed too challenging to me at the moment.
I'd very much appreciate your help. Thanks.
Well, when you dont take up new challenges you stop learning !
The onclick method looks like the best imo, lets see what others suggest.
honestly, using JS and AJAX to communicate with your django server might be dauting at first but it is quite easy really.
if you know how to create a function in your views.py and know a bit of JS, it's just like any other classic functionnality.
Set up your urls.py for the view function that will add a click to the counter:
path('ajax/add_click', views.add_click name="add_click"),
Then, create your view function (pseudo code):
def add_click(request):
# retrieve the article
article_id = request.GET.get("articleId", None)
# then retrieve the object in database, add 1 to the counter save and return a response
Now the "complicated" part, the ajax request:
function add_one_click(articleId) {
$.ajax({
type: "GET",
url: '/ajax/add_click', // you also can use {% url "app_name:add_click" %}
data: {
'articleId': articleId,
},
success: function() {
console.log("hit added to article");
}
});
}
You need to add JS and Ajax lib to your html template for it to works.
Also you need to pass in the onclick attribute the name of the function + the id of the article
onclick="add_one_click({{article.id}})"
One more thing, this type of view, if not protected can lead to get false results.
Instead of having q.url have a new URL(/article_count?id=q.id) which you will define on your Django project
def article_count(req):
_id = req.GET.get('id', '')
# Query Aritcle and get object
q = Article.objects.get(id=_id)
# update the fields for clicks
q.hits += 1
q.save()
# redirect the page
return redirect(q.url)
Edit:
Create a new url that would handle your article click, lets say-
path('article/clicked/<article_number>', views.click_handler, name='click_counter')
Now, in your template use this url for all the article
<div class="col text-center">
{% for q in allArticles %}
<h2 id={{q.number}}>{{q.title}}</h2>
{% endfor %}
</div>
and in your views.py create a new controller
def click_handler(request, article_number):
article = Article.objects.get(number=article_number)
article.hits += 1
article.save()
# now redirect user to the outer link
return redirect(article.url)

Django-taggit tag value retrieval and formatting failing

I am trying to implement a tagging process for profiles so you can add your hobbies for example.
I have chosen django-taggit as it seemed quite simple and does what I need it to, plus don't really know how to do it myself from scratch.
I have managed to make it work to some extent but I am having issues with 3 things:
Not really sure what's the best way to control the form field for these tags as I generate the form automatically with widget adjustments in meta function of the form, but it might work fine after resolving the below two issues.
When there is no data for the field hobbies (tags) the field gets populated with a single tag of value "[]" as per below image.
When I add a tag of "music" and submit the form after I reload the page I get this "[]" as per image. I assumed this will be dealt with by the library, but I cannot see another similar scenario online.
When I try adding another tag of "games" and save and reload, the below happens. The initial value gets wrapped again.
My model is:
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
hobbies = TaggableManager()
My form is:
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['hobbies',]
def __init__(self, *args, **kwargs):
super(UserProfileForm, self).__init__(*args,**kwargs)
self.fields['hobbies'].widget = forms.TextInput()
self.fields['hobbies'].widget.attrs['data-role'] = "tagsinput"
self.fields['hobbies'].widget.attrs['class'] = "form-control"
self.fields['hobbies'].required = False
My view function is:
if request.method == 'POST':
user_profile = UserProfile.objects.get(user=request.user)
form = UserProfileForm(request.POST, instance=user_profile)
print(form)
if form.is_valid():
obj = form.save(commit=False)
obj.user = request.user
obj.save()
print("Form valid")
form.save_m2m()
Using:
<script src="/static/js/tagsinput.js"></script>
<link rel="stylesheet" href="{% static 'css/tagsinput.css' %}" />
I had this exact same problem.
One solution is to apply the data-role="tagsinput" AFTER you turn a list of tags into a comma-separated string for the form.
Here is that solution:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, **kwargs):
self.fields['tags'].widget.attrs['value'] = ", ".join(list(self.instance.tags.names()))
self.fields['tags'].widget.attrs['data-role'] = "tagsinput"
Output:
As you can see, there's a problem with quotes appearing around tags that are multi-word. It also causes new tags with quotes to be saved to the database.
If double-quotes didn't appear around multi-word phrases, this would be the most elegant solution. If someone solves this in the future, drop a note!
My template is this:
<div class="m-3 p-3 border">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">Save Form</button>
</form>
</div>
I know I can use a template tag to strip the extra quotes from the tag field itself, but then I'd have to go through and create all the form fields manually just to set the tags template tag.
For the time being, my solution is to simply use Javascript and just modify the Meta widgets section of the form.
FINAL ANSWER (for now):
forms.py
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
widgets = {
'tags': forms.TextInput(attrs={
"data-role": "tagsinput",
})
}
custom.js - put this script on the page that loads the form.
document.addEventListener("DOMContentLoaded", function(event) {
let tags_input = document.querySelector('#id_tags');
let tags_input_value = tags_input.value;
let new_value = [...tags_input_value.matchAll(/<Tag:\s*([\w\s]+)>/g)].map(([, m]) => m).join(', ')
tags_input.setAttribute('value', new_value);
}
So all we're doing is modifying the front-end presentation, and leaving all the backend internal forms functionality untouched.
So after quite a few (hundreds) of tests, I finally narrowed down where the issue was and tried to go around it with successful result.
It seems the data got amended into tag objects through tagsinput library I was using. Only when the "data-role" was specified as "tagsinput" in the forms.py the data would already come to html side as those objects and be shown incorrectly. So instead I wanted to keep the data clean and only apply data-role='tagsinput' in the end for visual aspect, which I did using:
var hobbiesTags = document.getElementById("id_hobbies");
if(hobbiesTags){
var att = document.createAttribute("data-role");
att.value = "tagsinput";
hobbiesTags.setAttributeNode(att);
};
And that resulted in the below. Maybe there are better ways to do this, I'm not sure, but it's a pretty clean solution. Share your alternatives.

Readonly form field in edit view - Flask-Admin

I tried to set a form field as readonly just for the edit view in a Flask-Admin application. Following the answers to this question I know that if I have this
class MyView(ModelView):
form_widget_args = {
'title': {
'readonly': True
}
}
I can set a form field as readonly, but this is applied to both create and edit views. How can I apply an argument to the edit view only?
A pure python solution could be using the on_form_prefill callback function that admin provides, and it's only run in the edit view. You wouldn't need form_widget_args in that case.
You could have something like this to edit the form dynamically, making that field read only:
class MyView(ModelView):
def on_form_prefill(self, form, id):
form.title.render_kw = {'readonly': True}
I would do a little workaround for this. Create a custom edit template and add it to your class. The standard edit template you find in the flask repository on github (standard edit template)
class MyView(ModelView):
edit_template = 'my_custom_edit_template.html'
And in your custom edit template make a javascript function which disables your element. So it's only disabled in edit view and not in the create view.
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
<script>
window.onload = function () {
document.getElementById("myfield_id").disabled = true;
}
</script>
{% endblock %}
It's maybe not the best solution but it works for me. I think it should also be possible to change the jinja2 template to directly disable the filed. I tried: {{ form.myfield_id(disabled=True) }} but then it renders my field twice... but my first approach works.
Another way to do it is to use form_edit_rules and form_create_rules. Display the fields you only want in create view, and add the field you want to be displayed in edit view and make it read-only
form_create_rules = (
'col1', 'col2', 'col3')
form_edit_rules = (
'col1', 'col2', 'col3', 'col4')
form_widget_args = {
'col4': {
'disabled': True
}
}
I think you can do it by checking the value you are trying to edit in the method that handles edit.
For eg:
You are trying to edit a value with id 1, then in the views py file you can check for the that value and add the code.
def editdata(id=None):
if id:
form.title.render_kw = {'readonly': True}
#rest of the code

Changing entire background color of table cell in Django Admin

I'm trying to color an entire Django Table Cell and this following code only colorizes the text in the Django Admin and not the ENTIRE table cell. What is currently looks like:
Here's the current code
class Student(models.Model):
def city_and_zip(self):
if self.city_zip:
cell_html = '<div style = "background-color:#e6f2ff;">%s</div>'
else:
cell_html = '<div>%s</div>'
return cell_html % self.city_zip
city_and_zip.allow_tags = True
Again, it colorizes it properly... but only to some extent. I would like to color the entire cell. Changing div to th doesn't work either
When you declare list_display for modelAdmin, td element wich consist this field has css-class like field-'list_display_name'. I your case it will field-city_and_zip.
So you can add css selectors for this class. Django provides many solutions for this approach. Here is one:
class StudentAdmin(admin.ModelAdmin):
#property
def media(self):
media = super(ProductAdmin, self).media
css = {
"all": (
"css/your.css",
)
}
media.add_css(css)
return media
In other hand you can write class Media in your admin model:
class StudentAdmin(admin.ModelAdmin):
class Media:
css = {"all":("css/your.css",)}
Then put this css file in your static path and write selectors:
.field-city_and_zip {
background-color: #e6f2ff;
}
And it should work like you need. Also you can put css directly in admin base template
more information about admin page
You can change admin css assets in two ways:
Model level: Add required css files to your admin model.
class MyModelAdmin(admin.ModelAdmin):
class Media:
js = ('js/admin/my_own_admin.js',)
css = {
'all': ('css/admin/my_own_admin.css',)
}
Template level: If you want to change the appearance of the admin in general you should override admin templates. This is covered in details here: Overriding admin templates.
Sometimes you can just extend the original admin file and then overwrite a block like {% block extrastyle %}{% endblock %} in django/contrib/admin/templates/admin/base.html as an example.

Categories