There are many questions here on SO with titles that sound similar to what I'm about to describe but as far as I can tell from literally hours of research, this question is unique. So here goes!
I'm writing my first Flask app. I'm using SQLAlchemy for the model layer and WTForms to handle forms. The app is going to be a lightweight personal finance manager that I probably will not actually use for for serious biz. I have one table for a list of all transactions and another for all expense categories (groceries, clothing, etc). The transaction table has a column ("category") which references the Category table. In the view, I represent the list of categories with a element.
My issue is that when editing a transaction, I can't figure out how to tell WTForms to set the element to a specific pre-defined value. (Yes, I know that you can set a default value at the time that the form is defined, this is not what I am asking.)
The model looks like this (with irrelevant fields removed):
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False, unique=True)
# ...
class Trans(db.Model):
# ...
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
category = db.relationship('Category',
backref=db.backref('trans', lazy='dynamic'))
# ...
forms.py:
def category_choices():
return [('0', '')] + [(c.id, c.name) for c in Category.query.all()]
class TransactionForm(Form):
# ...
category = SelectField('Category', coerce=int, validators=[InputRequired()])
# ...
The route (POST not yet implemented):
#app.route('/transactions/edit/<trans_id>', methods=['GET', 'POST'])
def trans_edit(trans_id):
transaction = Trans.query.get(trans_id)
form = forms.TransactionForm(obj=transaction)
form.category.choices = forms.category_choices()
form.category.default = str(transaction.category.id)
#raise Exception # for debugging
return render_template('trans.html',
title='Edit Transaction',
form=form)
And finally, the template (Jinja2):
{{ form.category(class='trans-category input-medium') }}
As you can see in the route, I set form.category.default from the transaction.category.id, but this doesn't work. I think my issue is that I'm setting "default" after the form has been created. Which I'm rather forced to because the model comes from the database via SQLAlchemy. The root cause seems to be that form.category is an object (due to the relationship), which WTForms can't seem to handle easily. I can't have been the first one to come across this... Do I need to rework the model to be more WTForms compatible? What are my options?
Thanks!
I alluded to this in my comment. It sounds like you might benefit from using WTForm's SQLAlchemy extension. This will create a dropdown list for categories in your trans form.
My example's use case is slightly different. I am relating blog post's to topic. That is, many posts share one topic. I image in your case, many transactions share one category.
Form
from wtforms.ext.sqlalchemy.fields import QuerySelectField #import the ext.
def enabled_topics(): # query the topics (a.k.a categories)
return Topic.query.all()
class PostForm(Form): # create your form
title = StringField(u'title', validators=[DataRequired()])
body = StringField(u'Text', widget=TextArea())
topic = QuerySelectField(query_factory=enabled_topics, allow_blank=True)
models
The important part here is a) making sure you have the relationship correctly defined, and b.) adding topic to your init since you use it to create a new entry.
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80))
body = db.Column(db.Text)
# one-to-many with Topic
topic = db.relationship('Topic', backref=db.backref('post', lazy='dynamic'))
def __init__(self, title, body, topic):
self.title = title
self.body = body
self.topic = topic
class Topic(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
def __init__(self, name):
self.name = name
view
Nothing special here. Just a regular view that generates a form and processes submitted results.
#app.route('/create', methods=['GET', 'POST'])
#login_required
def create_post():
form = PostForm()
if form.validate_on_submit():
post = Post(title=form.title.data, body=form.body.data,
topic=form.topic.data)
db.session.add(post)
db.session.commit()
Topic.update_counts()
flash('Your post has been published.')
return redirect(url_for('display_post', url=url))
posts = Post.query.all()
return render_template('create_post.html', form=form, posts=posts)
template
Nothing fancy here either. Just be sure to render the field in the template like you would a basic field. No fancy loop required since WTForms Sqlalchemy extensions does all that for you.
{% extends "base.html" %}
{% block title %}Create/Edit New Post{% endblock %}
{% block content %}
<H3>Create/Edit Post</H3>
<form action="" method=post>
{{form.hidden_tag()}}
<dl>
<dt>Title:
<dd>{{ form.title }}
<dt>Post:
<dd>{{ form.body(cols="35", rows="20") }}
<dt>Topic:
<dd>{{ form.topic }}
</dl>
<p>
<input type=submit value="Publish">
</form>
{% endblock %}
That's It! Now my post form has a topic dropdown list. To use your terminology, when you load a transaction the default category for that transaction will be highlighted in the dropdown list. The correct way to state this is to say that the category associated with the transaction is loaded via the relationship defined in the trans model.
Also note, there is also a multisellect SQLAlchemy extension in case one transaction has many 'default' categories.
Now, my issue is how to deal with many-to-many relationships.... I'm trying to pass a string of tags that are stored in a many-to-many table to a TextArea field. No SQLAlchemy extension for that!
I posted that question here
Related
I am currently learning Python. I have an app with two classes - customer and order. I would like to display a page that shows all orders associated with one customer (many to one relationship). I believe that I have defined the relationship correctly, but I'm getting some errors.
models.py
from app import db, ma
class customer(db.model):
__tablename__="customer"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120))
orders = relationship("order", back_populates="customer")
def __repr__(self):
return '<customer %r' % self.id
class order(db.Model):
__tablename_="order"
id = db.Column(db.Integer, primary_key=True)
customer_id = db.Column(db.Integer, db.ForeignKey("customer.id"))
content = db.Column(db.String(120))
customer = relationship("customer")
def __repr__(self):
return '<order %r' % self.id`
routes.py
from app import app
from flask import Flask, render_template, request, redirect
from app.models import *
#app.route('/orders/<customer>')
def show_orders(name):
orders = order.query.join(customer, customer.id==order.customer_id).filter_by(customer.name==name).all()
return render_template('orders.html', name=name)
So for example, if I typed 'orders/bob_smith', this page should display all the orders made by bob_smith in the database.
But when I try to run this, I get the following error:
TypeError: filter_by() takes 1 positional argument but 2 were given.
Am I doing something wrong with the models? Or am I not assigning the right variables in filter_by? Would appreciate any help!
Edit: After some advice, I made the following change to routes.py
#app.route('/orders/<customer>')
def show_orders(name):
orders = order.query.join(customer, customer.id==order.customer_id).filter_by(customer.name==customer.name).all()
return render_template('orders.html', name=name)
I was able to see orders/bob_smith, but instead of showing me only bob_smith's orders, I saw all orders in the database.
orders.html
{{ name }} Orders
{% for order in orders %}
{{ order.customer }} # should all be bob_smith
{{ order.content }}
But instead, I'm seeing a list of all the orders. Am I not limiting the query sufficiently?
Just some small updates:
#app.route('/orders/<customer_name>')
def show_orders(customer_name):
orders = order.query.join(customer, customer.id==order.customer_id).filter(customer.name==customer_name).all()
return render_template('orders.html', orders=orders)
order.html will then need to have:
{% for order in orders %}
{{ order.customer }}
{{ order.content }}
{% endfor %}
I try to build a flask site with a very simple dictionary. A user can save words which are stored in a simple sqlite database. All that works fine, but if I try to implement a simple test on the vocabulary I canĀ“t get consistent data from the database. The site is called, and after validate_on_submit it is relaoded so my choice from the database is a new one. Sorry for the bad description, may be it is gettinger clearer if I show you the code. I followed the CoreySchaefer Tutorials and the structur, so
# forms.py
# ...
class VocTestForm(FlaskForm):
german = StringField('Deutsch', validators=[DataRequired()])
submit = SubmitField('Check')
# models.py
# ...
class Dictionary(db.Model):
id = db.Column(db.Integer, primary_key=True)
engl = db.Column(db.String(120), unique=True, nullable=False)
german = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f"{self.engl} {self.german}"
# voc_test.html
{% extends "base_layout.html" %}
{% import "bootstrap/wtf.html" as wtf%}
{% block content %}
<h1>{{check_word.engl}}</h1>
{{ wtf.quick_form(form) }}
{% endblock %}
# routes.py
# ...
#app.route('/voc_test', methods=['GET', 'POST'])
def voc_test():
form = VocTestForm()
content = Dictionary.query.all()
check_word = random.choice(content)
if form.validate_on_submit():
if str(form.german.data) == str(check_word.german):
flash(f'Correct', 'success')
return redirect(url_for('voc_test'))
else:
flash(f'wrong', 'danger')
form.german = " "
return render_template('voc_test.html', form=form, check_word=check_word
The last function is not working. If I compare the String from the form with the String from the database the random.choice has already chosen a new word. How can I prevent this?
Thanks for the help
Steffen
If I've read what you're trying to do right, you could use the random library to generate a number. Then pull the item at that index from the dictionary.
I would like to show possible choices from ManyToManyField (which I have in Homes model) in the Owners form. I have Owners <--Many2Many--> Homes with custom class HomesOwners. In Homes it works out of the box, I don't know how to make it work in Owners.
I am using Django 2.2.4 with Bootstrap 4 and Postgresql. I started my project based on django-bookshelf project (also just Django and Bootstrap4). I do not use any render. Comment in django-bookshelf project mentioned How to add bootstrap class to Django CreateView form fields in the template?, so I stick to that if it comed to forms.
I'm pretty new to Python (so Django too) and web technologies in general. I googled dozen of different questions/answers but I couldn't find any nice explanation of what is what and how to use it in real life. Most of them ended up with basic usage.
I did some experimentation on my own, but no success so far...
Here is the code
I have two models - Homes/models.py and Owners/models.py
Homes/models.py:
class Homes(models.Model):
id = models.AutoField(primary_key=True)
# other fields
some_owners = models.ManyToManyField(Owners, through='HomesOwners', through_fields=('id_home', 'id_owner'), related_name='some_owners')
# end of fields, some other code in the class like "class Meta" etc.
class HomesOwners(models.Model):
id = models.AutoField(primary_key=True)
id_home = models.ForeignKey(Homes, models.DO_NOTHING, db_column='id_home')
id_owner = models.ForeignKey('owners.Owners', models.DO_NOTHING, db_column='id_owner')
Owners/models.py do not have anything special, no imports from my Homes/models.py etc.
and my forms:
Homes/forms.py:
class HomesForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(HomesForm, self).__init__(*args, **kwargs)
self.fields['some_field_from_homes_model'].widget.attrs = {'class': 'form-control '}
#
# --> no need self.fields for M2M, Django does the work
#
# but I tried also and have a --> Question 2
# self.fields["some_owners"].widget = forms.widgets.CheckboxSelectMultiple()
# self.fields["some_owners"].queryset = HomesOwners.objects.all()
Without any code as "self.fields" for M2M field, Django is able to generate for me list of owners.
Question 1
I would like to get list of Homes in my OwnersForms.
I do not know what to add. I assume that I cannot add
# Owners/models.py
some_homes = models.ManyToManyField(Homes, through='HomesOwners', through_fields=('id_home', 'id_owner'), related_name='some_homes')
because of circular import, am I right?
How do I get my Homes list using self.fields?
What do I need to add to my code?
Question 2
When I've added
# Homes/forms.py
self.fields["some_owners"].widget = forms.widgets.CheckboxSelectMultiple()
self.fields["some_owners"].queryset = HomesOwners.objects.all()
I got
<!-- html page -->
HomesOwners object (1)
HomesOwners object (2)
<!-- and so on... -->
How can I just list Owners?
How to filter/order them so first they would appear Owners not connected to any Home?
Question 3
class HomesOwners(models.Model):
id = models.AutoField(primary_key=True)
id_home = models.ForeignKey(Homes, models.DO_NOTHING, db_column='id_home')
id_owner = models.ForeignKey('owners.Owners', models.DO_NOTHING, db_column='id_owner')
def __str__(self):
return pass #return something
I can't get my head around this. This class connects Homes and Owners. When I'm thinking of Homes I would like to return Owners and vice versa. So it should return different things depending on what object we are using (home or owner). I think this is connected to my 2nd question about:
HomesOwners object (1)
Also...
In homes.html I'm using my M2M like that:
{% for owner in homes.homesowners_set.all %}
{{ owner.id_owner.id }}
{% endfor %}
I would like to write something similar to my owners.html and list homes. This is connected to my previous question, I would like to have full answer if that's possible.
EDIT
With the answer given to me I was able to add Homes to OwnerUpdate view. I have views like that:
owners/views.py
# List View
class OwnersList(ListView):
model = Owners
# Detail View
class OwnersView(DetailView):
model = Owners
# Create View
class OwnersCreate(CreateView):
model = Owners
form_class = OwnersForm
# Setting returning URL
success_url = reverse_lazy('owners_list')
# Update View
class OwnersUpdate(UpdateView):
model = Owners
form_class = OwnersForm
success_url = reverse_lazy('owners_list')
# Delete View
class OwnersDelete(DeleteView):
model = Owners
success_url = reverse_lazy('owners_list')
What change do I need to make to be able to show in OwnersList Homes they own?
In Homes DetailView I am able to show Owners. I would like to do the same for Homes' DetailView and Homes ListView.
I don't really get it what you asking for,
but if I understand your question correctly,
I assume you want to add Homes list (not HomesOwners list) into your Owners form, right?
you can add extra field in your form like this:
class OwnersForm(ModelForm):
# notice the queryset is 'Homes' not 'HomesOwners'
homes = forms.ModelMultipleChoiceField(queryset=Homes.objects.all(), widget=forms.CheckboxSelectMultiple)
class Meta:
model = Owners
fields = ('homes', 'your_other_fields',)
# then you can access in init function too if you want
def __init__(self, *args, **kwargs):
super(OwnersForm, self).__init__(*args, **kwargs)
self.fields['homes'].required = False
then, since it using CheckboxSelectMultiple widget, you can iterate it in your html template like this:
{% for key, value in your_form.homes.field.choices %} <!-- notice '.field.'-->
{{ key }} = {{ value }}
{% endfor %}
you probably need to create custom save too for your form.
for your question 3, it is not about the form?
If you want to show HomesOwners, you are already doing right.
{% for owner in homes.homesowners_set.all %}
{{ owner.id_owner.id }}
{% endfor %}
but it will work if that homes is only 1 object.
if homes is a queryset, you have to iterate it first
{% for home in homes %}
{% for owner in home.homesowners_set.all %}
{{ owner.id_owner.id }}
{% endfor %}
{% endfor %}
sorry if I misunderstanding your questions,
maybe you can provide your views.py too, so I or others can help you more specific
I am relatively new to Django and I made a Todo list where user can add a task and mark if its completed. I added a form field of priorities which is a radio select widget. Based on the priority the task field will have red, orange or green color.
The radio buttons appear correctly and I cant post a task without giving an input priority. But the priority is always taken as default(high).
I tried a couple of things to change and display the priorities but nothing worked.
I believe something in the views.py is to be modified to make it work but due to my lack of experience I cannot put a finger on it.
Views.py
#require_POST
def addTodo(request):
form = TodoForm(request.POST)
#print(request.POST['text'])
if form.is_valid():
new_todo = Todo(text = request.POST['text'])
new_todo.save()
for item in form:
return redirect('index')
def completeTodo(request, todo_id):
todo = Todo.objects.get(pk=todo_id)
todo.complete = True
todo.save()
return redirect('index')
form.py
from django import forms
prior_choice =[('high','High'),('mod','Mod'),('low','Low')]
class TodoForm(forms.Form):
text = forms.CharField(max_length = 40,
widget = forms.TextInput(
attrs= {'class': 'form-control', 'placeholder': 'Enter todo e.g. Delete junk files', 'aria-label': 'Todo', 'aria-describedby':'add-btn'}))
priority = forms.CharField(widget=forms.RadioSelect(choices=prior_choice))
models.py
from django.db import models
class Todo(models.Model):
text = models.CharField(max_length=40)
complete = models.BooleanField(default = False)
task_priority = models.CharField(max_length=40, default='high')
def __str__(self):
return self.text
index.html
<ul class="list-group t20">
{% for todo in todo_list %}
{% if todo.task_priority == 'high'%}
{{ todo.text}}</li>
{%elif todo.task_priority == 'mod'%}
{{ todo.text}}</li>
{%elif todo.task_priority == 'low'%}
{{ todo.text}}</li>
{%else%}
<div class="todo-completed"> <li class="list-group-item" style="background-color: green;"> {{ todo.text}}</li></div>
{%endif%}
{% endfor %}
</ul>
Heres a screenshot of the output app
Please help me link the radio button to a task in the list and display accordingly.
Thanks in advance.
The problem is in your view. While you are creating your Todo object you are not passing the priority.
new_todo = Todo(text = request.POST['text'], task_priority = request.POST['priority'])
The code above solves your problem. But I DO NOT RECOMMEND it. You are not leveraging the Django forms. Please use Django forms.cleaned_data to get parameters instead of request.POST or use ModelForm which will allow you to save from form instance directly.
Model Change Advice
However this is not how i would like solve the issue. You can change your model as following to have more djangoic way of doing it:
from django.utils.translation import ugettext_lazy as _
class Todo(models.Model):
PRIORITY_NONE = 0
PRIORITY_LOW = 1
PRIORITY_MODERATE = 2
PRIORITY_HIGH = 3
PRIORITIES = (
(PRIORITY_NONE, _('')),
(PRIORITY_LOW, _('Low')),
(PRIORITY_MODERATE, _('Moderate')),
(PRIORITY_HIGH, _('High')),
)
...
task_priority = models.PositiveSmallIntegerField(choices=PRIORITIES, default=PRIORITY_NONE)
You may need to change your form with the choices Todo.PRIORITIES. Also you may want to use ModelForm which will make things much easier for you.
I feel like I'm really close, but not quite there yet. Please bear with me as I am very much so in the beginner stages of learning django.
I have a feature where users can comment on each blog post. I want to display the total number of comments each user has next to his name. If that user has left 4 comments on 4 different posts, I want it to show "4 total comments" next to his name on each one of his individual comments throughout my website.
I have made a model method that I put in my view, which automatically updates the total comments for each user. The only problem is that if the user has left two comments, only his latest comment will show "2 total comments". His previous one shows only "1".
My question is, how do I make the previous entry update when the user has left a new comment?
models.py
class Comment(models.Model):
...
post = models.ForeignKey(Post, related_name="comments")
user = models.ForeignKey(User, related_name="usernamee")
email = models.EmailField(null=True, blank=True)
picture = models.TextField(max_length=1000)
...
review_count = models.IntegerField(default=0)
class UserProfile(models.Model):
...
def user_rating_count(self): #This is what adds "1" to the user's total post count
user_ratings =
Comment.objects.all().filter(user_id=self.user.id).count()
user_ratings += 1
return user_ratings
views.py
#login_required
def add_comment(request, slug):
post = get_object_or_404(Post, slug=slug)
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.user = request.user
comment.email = request.user.email
comment.picture = request.user.profile.profile_image_url()
comment.review_count = request.user.profile.user_rating_count() #This is where I'd like it to update, but it doesn't seem to work this way
comment.save()
return redirect('blog:post_detail', slug=post.slug)
else:
form = CommentForm()
template = "blog/post/add_comment.html"
context = {
'form': form,
}
return render(request, template, context)
template
{% for comment in post.comments.all %}
<p>{{ comment.user.first_name }} <b>{{ comment.user.last_name }}</b> {{ comment.review_count }}</p>
{% endfor %}
User comments once = FirstName LastName 1.
User comments twice, his second comment = FirstName LastName 2, but the first comment remains unchanged.
Any ideas on how to properly do this? Any help is greatly appreciated!
First i don't think you need the review_count to be a Database field. Except you plan to use it to sort (or do something that requires it to be in the Database).
From your question "django forms - how to update user data from previous comment when user posts a new comment" I believe you have an idea on why it's not working.
It's because it's a previous comment and Updating the data of the latest comment won't automatically update the previous comments (It would be disastrous if that happened by default :-) )
Anyway once you remove thereview_count and make user_rating_count a property of the UserProfile model your problem disappears.
class UserProfile(models.Model):
#property
def user_rating_count(self):
"""This is what adds "1" to the user's total post count"""
return self.usernamee.count() # Remember the `related_name` you set earlier?
Then you can use it in your templates like this
{% for comment in post.comments.all %}
<p>{{ comment.user.first_name }} <b>{{ comment.user.last_name }}</b> {{ request.user.profile.user_rating_count }}</p>
{% endfor %}
If you're bothered about the value being recalculated on each page load (You should be). Django provides a nifty decorator to cache the property in memory and reduce the load on your database (When you call the method/access the property repeatedly)
from django.utils.functional import cached_property
class UserProfile(models.Model):
#cached_property
def user_rating_count(self):
"""This is what adds "1" to the user's total post count"""
return self.usernamee.count() # Remember the `related_name` you set earlier?
If it doesn't need to be a Database field, it doesn't need to be. You can easily make it a computed property and Cache it (If you feel it's expensive to recalculate everytime and you don't really care about the "freshness" of the data).
By the way if you needed to update the Old comments (The way you wanted to initially), You would've done a batch update like this
I'm being overly verbose here:
comments = Comment.objects.filter(user_id=self.user.id)
count = comments.count()
comments.update(review_count=count)
That will update all the comments that match the filter params. But like i said i don't think this is the best approach for what you want to do.
Read up
https://docs.djangoproject.com/en/1.11/ref/utils/#django.utils.functional.cached_property
and
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#update