Listing and Creating Groups

Diving deeper into Jinja2 templates, we will add a list of existing groups to the homepage and allow the user to create new groups.

Listing and Creating Groups

Diving deeper into Jinja2 templates, we will add a list of existing groups to the homepage and allow the user to create new groups.

At the moment the application is in the early stage of development so the homepage simply contains a simple form that allows the user to state a nickname. Later we will create authentication, for example with a password, that will stop anyone bar the person from accessing a nickname’s groups. That’s not necessary yet.

To list any groups that the user may have created already, we need to access the database via the model. The web controller will do this and add the data to the model for it to be rendered by the Jinja template.

Look up the user (line 6 in the following code block), and then use that id to search the group table. It is possible that the nickname will not have been added to the user table yet, for example, a new user that has not created a group yet, so on line 4 we define the groups list as empty.

@web.route('/', methods=['GET'])
def index():
	nickname = session.get('nickname')
	groups = []
	if nickname != None:
		user = User.query.filter_by(nickname=nickname)
		if user.count() > 0:
			groups = Group.query.filter_by(user=user.first().id).all()

	form = NickNameForm()
	return render_template('index.html', form=form, nickname=nickname, groups=groups)

Then we can show a simple list of the groups on the page using a for loop in Jinja templating language. It mixes HTML and template commands as follows:

<ul>
	{% for group in groups %}
		<li>{{ group.name }}</li>
	{% endfor %}
</ul>

This is what the page will look like for a user with a few groups:

list_of_groups

Next we will add another button to the page to allow the user to create a group. To do this we need to first create a new form class that represents the action of creating a group. The user nickname will be available in the session; it will be required as the foreign key from group table to user table.

class NewGroupForm(FlaskForm):
    name = StringField('Enter the name of the new group', validators=[DataRequired()])
    submit = SubmitField('Create Group')

We can add the form using the same short cut as the last time. Note that the action here needs to send the data to a different location. We are going to use a controller in the web blueprint for the time being, but ultimately this action might be better placed into the API.

<h3>To create a new group, enter a name and click the button.</h3>
{{ wtf.quick_form(new_group_form, action="creategroup") }}

Note that I’ve moved the import of bootstrap/wtf.html to the start of the template as it is used in two places now.

Also note that the use of the action="creategroup" parameter. This is necessary as the otherwise the current URL will be used to send the request to.

The controller code for the index function needs to create the form and add it to the model so that it can be rendered:

...
new_group_form = NewGroupForm()
return render_template('index.html', form=form, nickname=nickname, groups=groups, new_group_form=new_group_form)

Here is what the new button looks like:

create_new_group

We need to create a new function to handle the request to controller.py to add the group. This is going to redirect back to the index page (discussed in Posting with Flask WTForms). We have to:

  • get the user nickname from the session (line 5)
  • find out if it exists (lines 6 and 7)
  • create a new user if not (lines 8 and 9)
  • find out if the group exists (lines 13 and 14)
  • indicate a duplicate by redirecting to a different page (lines 15 to 18)
  • create a new group if otherwise (lines 19 an 20)
  • commit changes (line 21)
  • and redirect to the index page (line 22)
@web.route('/creategroup', methods=['POST'])
def create_group():
	form = NewGroupForm()
	if form.validate_on_submit():
		nickname = session.get('nickname')
		user_query = User.query.filter_by(nickname=nickname)
		if user_query.count() == 0:
			user = User(nickname)
			db.session.add(user)
		else:
			user = user_query.first()
		group_name = form.group_name.data
		group_query = Group.query.filter_by(name=group_name, user=user.id)
		if group_query.count() > 0:
			error_message = 'That group already exists. Please try a different name.'
			back_url = url_for('web.index')
			return render_template('generic-error.html', error_message=error_message, back_url=back_url), 400
		else:
			group = Group(group_name, user.id)
			db.session.add(group)
	db.session.commit()
	return redirect(url_for('web.index'))

The final bit is to add the error page. I’ve decided to create a generic error page and pass a message and a link back to the original request page. Flask also has some ways of intercepting exceptions using the errorhandler annotation that I may use later.

For now, I simply set two model properties for error_message and back_url and show them on-screen. This is in templates\generic_template.html.

{% extends "bootstrap/base.html" %}
{% block title %}Agile Anonymous Estimation{% endblock %}
{% block content %}
	<div class="container">
		<h1>There was a problem with your request.</h1>
	</div>

	<div class="container">
		{% if error_message %}
			<p> {{ error_message}} <p>
		{% endif %}

		{% if back_url %}
			<a href='{{ back_url }}'>Go back to the previous page</a>
		{% endif %}
	</div>
{% endblock %}

Here is what happens if I try to create NewGroup a second time for this nickname:

duplicate_group

I can create a new group called It Works!. Enter the name into the form:

creating_new_group

The new group appears after the successful redirect to the index page which now show the new group:

it_works

Next

I’ve fallen behind on doing test driven development. The commit associated with this post will include some fixes for existing tests, but code coverage has fallen behind.

An issue I am having trouble with is setting up the test context to act as if I have a nickname in the session. That’s what I will cover next to get back up to full code coverage.