Minimum Viable Product

A minimum viable product allows you to demo the app, verify any assumptions and have a decision whether to continue development or cut losses.

Minimum Viable Product

A minimum viable product allows you to  demo the app, verify any assumptions and have a decision whether to continue development or cut losses.

Big projects often suffer because they do not deliver until the end. That means that no value is returned until all the pieces click together. If you got any decisions wrong up front, it will cost a lot to change afterwards.

An alternative approach is to strive to deliver the minimum version of the product or service that can sell or be of use. By defining the success criteria up front, you can decide early if the project is worth continuing.

With that in mind, I’ve made progress with the Anonymous Agile Estimation application, getting it to the point where I can demo the functionality and see if it’s useful.

I have concentrated on the web, leaving the REST side of the application to be tackled later. Also I have continued to postpone adding proper security for now, but there is enough code to demonstrate the functionality.

This post contains some of the things that I’ve learned in the process.

Setting the default field value in a form

When everyone in a group has given their secret estimate, the group leader can then expose the result. Rather than show all the votes, the application will calculate the average, find the nearest integer and the nearest Fibonacci number and show these to the user.

The group can decide if the result is fair, possibly sparking some discussion. They can choose to accept the combined estimate or to enter a completely new one.

My team uses Fibonacci numbers so I have decided that the number that is pre-populated in the form should be the nearest in the series. The estimates are limited to numbers between 1 and 100 so only those in this range will be used.

I'm adding 100 to the list to allow a maximum to be picked, potentially indicating that the scale the team is using is wrong, or the user story needs to be broken up.

Once these numbers have been calculated, how do you set the field in the form? At form construction stage, the field can be given a default value, but this is of little use as the estimates are not collected at that point.

The default property is available to set at runtime, but there is a trick to this. You also have to process the form to pick up the new value. Here is the code:

estimate_form = LockEstimateForm()
estimate_form.estimate.default = nearest_fib
estimate_form.process()

Picking a different action

The team may not be able to agree on the estimate. Perhaps some members gave large estimates, believing the task to be larger than it was. After some discussion, these issues might become resolved.

The calculated Fibonacci estimate, however, will be some way off where it should be. A discussion might result in consensus, but what group may decide is that they want to start the estimation process again? The application needs to let the group leader start over.

There are two ways to do this. I could have a separate button to reset all the estimates and start the process from scratch. The other is to allow the group owner to change a control on the form to select a different action.

A radio button option is one valid way of selecting an alternate action. In the field definition, specify a choices option that contains a list of tuples containing the value and the display option.

For example, to add a choice to lock in the estimate or to start over, with the former as the default, I added:

choice = RadioField('Lock this estimate or start over?', choices=[(1, 'Lock Estimate'), (2, 'Start Over')], default=1, validators=[DataRequired()])

However, I don’t like the feel of this in practice. I’d prefer two different buttons, i.e. different forms with alternative actions, so that the user has a clear idea of the action that they are taking. But it was interesting to see how easy it is to add different controls to the form. There are many more to explore.

Accessing Bootstrap button classes

Bootstrap provides a lot of nice styles to represent different purposes, such as buttons that look better than the standard one. This gives the page a nicer look and feel. For example the class: btn, btn-large, btn-success and btn-warning. The later two changing the colour to green or orange, respectively.

This is straightforward to add to HTML. To add a button to start the review process for an issue all over again that uses a warning colour, orange, use:

<a href="/issue/{{ issue.id }}/startover" class="btn btn-large btn-warning">Start Over</a>

What if you want to use the wtf.quick_form to generate the form rather than building the HTML yourself? This is easy too. There are a number of additional named arguments that can change the default classes used. Here is how I used the button_map optional argument to add styles to the “Lock Estimate” button:

{{ wtf.quick_form(estimate_form, action="/issue/{}/lock".format(issue.id), button_map={'submit': 'btn btn-large btn-success'}) }}

Resetting the data

No matter what choice is made, the estimates given by each member of the group are no longer necessary. Code should be added to delete these rows from the estimate table in the database.

The approach I’ve used is to use the SQLAlchemy filter_by to get search for all estimates associated with the issue. Then I can invoke the delete function.

def remove_estimates(issue_id):
	estimates = Estimate.query.filter_by(issue_id=issue_id)
	estimates.delete()
	db.session.commit()

This is called both from the function to lock the estimate in and also to start over.

Adding a Bootstrap navigation bar

I wanted to add a simple navigation bar to give all the pages a similar look and feel and to give a means to get back to the home page. With Bootstrap as the basis, this is straightforward. Note that several of the options will only show if the nickname is in the session object. Later I will replace this with proper authenticated login functionality.

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" href="/">Agile Anonymous Estimation</a>
    </div>
    <ul class="nav navbar-nav">
      {% if session['nickname'] %}
      <li><a href="/">My Groups</a></li>
      <li><a href="/confirm-logout">Sign Out {{ session['nickname'] }}</a></li>
      {% endif %}
    </ul>
  </div>
</nav>

The above classes use version 3 of Bootstrap classes which is the version imported with Flask-Bootstrap dependency.

Using template inheritance

Rather than add the navigation bar to every template, the application will make use of template inheritance. A base template, which itself extends bootstrap/base.html will hold the navigation bar and also a common title. Header details can also be added to the base template as required.

{% extends "bootstrap/base.html" %}

{% block title %}Agile Anonymous Estimation{% endblock %}

{% block navbar %}
<!-- code from navbar above -->
{% endblock %}

{% block content %}
{% endblock %}

This is a clever feature of the jinja templating system. Child templates can override part or all of the defined blocks and even include the super blocks if necessary.

The templates all change to inherit from this template instead. Also they do not need their own title elements any longer, unless it needs to be changed.

Error handling

While testing the new look and feel of the application, I noticed that the default error page that appears, for example, when a broken link is followed, does not fit in with the rest of the application.

It would be better to return the generic error template for the app, which inherits from the base template. This gives the user links back to home or to their groups page.

This is easy to do with Flask. You can use a decorator to mark a function that should deal with a particular error message.

@web.app_errorhandler(404)
def wrong_page(err):
	error_message = 'The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. Apologies for this inconvenience.'
	return render_template('generic-error.html', error_message=error_message), 404

Note that if you are not using blueprints then the decorator applies to the app object, for example, @app.errorhandler(404).

Adding logout option

A final point to note is how the log out functionality works. In the section on the navigation bar above, I mentioned that the code checks to see if the nickname session key is set to decide whether to show certain buttons.

This whole functionality will be changed ultimately, but for now to remove the field from the session it needs to be popped off the stack. Here is the controller function that handles the confirm page and the action:

@web.route('/confirm-logout', methods=['GET', 'POST'])
def confirm_logout():
	if request.method == 'GET':
		return render_template('confirm-logout.html')
	session.pop('nickname')
	return redirect(url_for('web.index'))

You can see the latest code for this application on GitHub.