Repository: learningequality/kolibri

Issue:
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: content_contentnode_tags.id
## Summary

When I'm trying to import a channel after another one, the importing failed with the error listed in `Traceback` section

## System information

*Please specify the Kolibri version you were using and your operating system!*

 - Version: kolibri-0.6.dev020170921163803-git.pex from https://storage.googleapis.com/le-downloads/kolibri%2Frelease%2F2632%2Freport.html
 - Operating system: Mac OS
 - Browser: Google Chrome

I also tested on Ubuntu 14.04 with Firefox and got the same issue

## Traceback or relevant snippet from server.log or browser console
```
INFO     Importing ContentTag data
INFO     Importing ContentNode_has_prerequisite data
INFO     Importing ContentNode_related data
INFO     Importing ContentNode_tags data
Traceback (most recent call last):
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 162, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/Users/lingyiwang/Documents/Learning Equality/kolibri/kolibri/__main__.py", line 8, in <module>
    main(args=sys.argv[1:])
  File "kolibri/utils/cli.py", line 580, in main
    manage(command, args=django_args)
  File "kolibri/utils/cli.py", line 411, in manage
    execute_from_command_line(argv=argv)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/django/core/management/__init__.py", line 353, in execute_from_command_line
    utility.execute()
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/django/core/management/__init__.py", line 345, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/django/core/management/base.py", line 348, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/django/core/management/commands/runserver.py", line 54, in execute
    super(Command, self).execute(*args, **options)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/django/core/management/base.py", line 399, in execute
    output = self.handle(*args, **options)
  File "kolibri/core/webpack/management/commands/devserver.py", line 61, in handle
    update_channel_metadata()
  File "kolibri/content/utils/annotation.py", line 31, in update_channel_metadata
    import_channel_from_local_db(channel_id)
  File "kolibri/content/utils/channel_import.py", line 305, in import_channel_from_local_db
    import_manager.import_channel_data()
  File "kolibri/content/utils/channel_import.py", line 196, in import_channel_data
    unflushed_rows = self.table_import(model, row_mapper, table_mapper, unflushed_rows)
  File "kolibri/content/utils/channel_import.py", line 184, in table_import
    self.destination.session.bulk_insert_mappings(DestinationRecord, data_to_insert)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2463, in bulk_insert_mappings
    return_defaults, False, render_nulls)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2536, in _bulk_save_mappings
    transaction.rollback(_capture_exception=True)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/util/langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2531, in _bulk_save_mappings
    isstates, return_defaults, render_nulls)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 69, in _bulk_insert
    bookkeeping=return_defaults)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 799, in _emit_insert_statements
    execute(statement, multiparams)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 945, in execute
    return meth(self, multiparams, params)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/sql/elements.py", line 263, in _execute_on_connection
    return connection._execute_clauseelement(self, multiparams, params)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1053, in _execute_clauseelement
    compiled_sql, distilled_params
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1189, in _execute_context
    context)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1402, in _handle_dbapi_exception
    exc_info
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/util/compat.py", line 203, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1159, in _execute_context
    context)
  File "/Users/lingyiwang/.venvs/kolibri/lib/python2.7/site-packages/sqlalchemy/engine/default.py", line 467, in do_executemany
    cursor.executemany(statement, parameters)
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: content_contentnode_tags.id [SQL: u'INSERT INTO content_contentnode_tags (id, contentnode_id, contenttag_id) VALUES (?, ?, ?)'] [parameters: ((1, u'7a8797a4dc4a412b8854c8111a93c0a1', u'0e70b89d2b454c0f83b21148197c8391'), (2, u'7a8797a4dc4a412b8854c8111a93c0a1', u'88bf5d8352a84b0ca3eb9da49f023fbd'), (3, u'7a8797a4dc4a412b8854c8111a93c0a1', u'04f845ad365844a88177141ecbb9e9cc'), (4, u'7a8797a4dc4a412b8854c8111a93c0a1', u'9fac0eefbe2c49b180c6cfbc8ef71ca0'), (5, u'6b3d90cc49e94b9fab7d691099beaa35', u'1eb2b903266e42e092ae0259ba71ff3a'), (6, u'6b3d90cc49e94b9fab7d691099beaa35', u'0e70b89d2b454c0f83b21148197c8391'), (7, u'6b3d90cc49e94b9fab7d691099beaa35', u'88bf5d8352a84b0ca3eb9da49f023fbd'), (8, u'194b533cc6e041028926a718a53eb6bc', u'6e80aec1d2164403a1e2133062e35961')  ... displaying 10 of 86 total bound parameter sets ...  (85, u'9d78f4242202494a82b6ac8d3553c74a', u'968d25449ae24189a5d7cbd6d90f1088'), (86, u'9d78f4242202494a82b6ac8d3553c74a', u'9fac0eefbe2c49b180c6cfbc8ef71ca0'))]
INFO     Closing webpack process
```

## How to reproduce

1. Run kolibri with `python kolibri.pex start`
2. Import channel with id `8b4d3e6d3d4842ba8ea658335b5dd252` from the internet
3. After the first import is done, start to import channel with id `b3556676589f4c408142da1d0f06cebc`
4. After it shows failure of import, do `python kolibri.pex stop`, the error will be shown
"Transfer failed" error on import from UI
### Observed behavior

Observed on several instances of the 0.6 release ([demo server](http://le-060-beta.learningequality.org), see discussion [in another issue](https://github.com/learningequality/kolibri/issues/2419#issuecomment-335950339)), and dev builds ([most recently here](https://github.com/learningequality/kolibri/pull/2494#issuecomment-338353686)): importing more than one channel fails with this unhelpful message. Import from the command line works fine.

![manage device content - kolibri - google chrome_688](https://user-images.githubusercontent.com/1457929/31865843-e238fbc8-b775-11e7-8ea0-051590eeae95.png)


### Expected behavior

Being able to import more than one channel.

### Errors and logs

None :unamused: 

### Steps to reproduce

Try to import more than one content channel from the UI.

### Context

* Kolibri version: release 0.6
* Operating system: all
* Browser: Chrome & FF

cc @rtibbles 



Discussion:
@aronasorman @rtibbles 
cc @aronasorman @jonboiser any thoughts?
@radinamatic did you see any traceback on the CLI when the error occurred?
Additional info: This has been uncaught by tests, we should reproduce it in a test..
> any traceback on the CLI when the error occurred?

None, zilch, nada, cap, nista... :confused: 
My guess is something is happening in the frontend to misread the task?
Can you paste the 'preview' when clicking on the tasks request in the network tab?
Hmmm, I can't seem to reproduce the error on the PEX from the [same build as in this comment](https://github.com/learningequality/kolibri/pull/2494#issuecomment-338353686). 
Will fire up a Windows VM to retry. 

Ah, and btw @rtibbles, admin in this build is being logged out, and not even after 10 minutes... :confused: 
 
Yeah, the timeout fixes have not propagated to 0.6 yet, they are coming.
I can reproduce this, happens when important the second channel, no useful log output:

```
INFO     Downloading data for channel id 8b4d3e6d3d4842ba8ea658335b5dd252
INFO:kolibri.content.management.commands.importchannel:Downloading data for channel id 8b4d3e6d3d4842ba8ea658335b5dd252
^M  0%|          | 0/154624 [00:00<?, ?it/s]^M100%|##########| 154624/154624 [00:00<00:00, 801173.16it/s]INFO     Importing ContentTag data
INFO:kolibri.content.utils.channel_import:Importing ContentTag data
INFO     Importing ContentNode_has_prerequisite data
INFO:kolibri.content.utils.channel_import:Importing ContentNode_has_prerequisite data
INFO     Importing ContentNode_related data
INFO:kolibri.content.utils.channel_import:Importing ContentNode_related data
INFO     Importing ContentNode_tags data
INFO:kolibri.content.utils.channel_import:Importing ContentNode_tags data
```
Has the transfer actually failed, or is the frontend just saying that it has failed? 
Yes, it failed, my login also timed out, got distracted by another potential issue, will return with more :)
Yeah, any traceback would be good! 
Login timeout should now be fixed on 0.6
Possible scenario:

1. Download Channel A (takes a while)
1. Finishes
1. 4½ minutes pass, I come back and enter the second channel
1. Login times out while import process has just launched

Also, I can be logged out while importing the second channel, this could be because login session isn't kept alive as it should after importing the first channel?

![image](https://user-images.githubusercontent.com/374612/31951116-8384f712-b8dd-11e7-8e58-b65521558aad.png)

Here's what I tried:
1. Created a new Vagrant VM running Ubuntu Zesty64
1. Created a new Kolibri pex file from the latest u/release-0.6.1. Commit: `c1378bb664ca7f949ba71041a5b32f668fd3437d`
1. Started said pex file from inside the Vagrant VM
1. Created a self-manged installation, and an admin account
1. Imported `8b4d3e6d3d4842ba8ea658335b5dd252` first. Success.
1. Imported `8b4d3e6d3d4842ba8ea658335b5dd252` again. Success.
1. Import portion control: `fb51dae6df7545af8455aa3a0c32048d`. Success.
1. Import a small test channel: `4d8725009c1d4c6fa81f61d5c2a51f3d`. Success
1. Import one of the MagoGenie channels: `8e06f21efdf23ffab4b026f674a8c442`. Success

Maybe @radinamatic or @benjaoming either of you can join me in a screenshare session where we can try to replicate this live?
@benjaoming Is a better bet, since I've killed the VM where the issue appeared last... :disappointed: 
Or @benjaoming can tell me which installer he used, I can grab it for the session with @aronasorman...?
@aronasorman try the exact same PEX as @radinamatic linked ( https://github.com/learningequality/kolibri/pull/2494 ) -- since we are looking for something that has been confirmed several times, but has not been consistently reproduced, we have to have as few unknowns as possible :/

I can say this much: I encountered it when importing `bcd99d8aeef04ce6b9e25a88d87eedb7` (demo) followed by `8b4d3e6d3d4842ba8ea658335b5dd252` (KA test)
Will try to reproduce and get back!
Weird, because I did try to repro it with that same PEX 2 days ago, to no avail... I then tried with Windows installer from the same build, no luck either. I even put in the sweet time to import manually the full CK-12 channel (6GB) and then try some re-import over it, still nothing... :rage: 

Not sure what is more annoying, seeing the bug all the time when it's messing your workflow, or not being able to repro it when you actually want to debug it :roll_eyes: 

Resolving patch:
diff --git a/kolibri/__init__.py b/kolibri/__init__.py
--- a/kolibri/__init__.py
+++ b/kolibri/__init__.py
@@ -6,7 +6,7 @@
 
 #: This may not be the exact version as it's subject to modification with
 #: get_version() - use ``kolibri.__version__`` for the exact version string.
-VERSION = (0, 6, 0, 'alpha', 0)
+VERSION = (0, 6, 0, 'final', 0)
 
 __author__ = 'Learning Equality'
 __email__ = 'info@learningequality.org'
diff --git a/kolibri/content/utils/channel_import.py b/kolibri/content/utils/channel_import.py
--- a/kolibri/content/utils/channel_import.py
+++ b/kolibri/content/utils/channel_import.py
@@ -5,6 +5,7 @@
 from kolibri.content.legacy_models import License
 from kolibri.content.models import CONTENT_SCHEMA_VERSION, NO_VERSION, ChannelMetadata, ContentNode, ContentTag, File, Language, LocalFile
 from kolibri.utils.time import local_now
+from sqlalchemy.exc import SQLAlchemyError
 
 from .annotation import set_leaf_node_availability_from_local_file_availability
 from .channels import read_channel_metadata_from_db_file
@@ -21,6 +22,12 @@
     Language,
 ]
 
+def column_not_auto_integer_pk(column):
+    """
+    A check for whether a column is an auto incrementing integer used for a primary key.
+    """
+    return not (column.autoincrement == 'auto' and column.primary_key and column.type.python_type is int)
+
 class ChannelImport(object):
     """
     The ChannelImport class has two functions:
@@ -56,7 +63,7 @@ def __init__(self, channel_id):
         # Use this rather than get_models, as it returns a list of all models, including those
         # generated by ManyToMany fields, whereas get_models only returns explicitly defined
         # Model classes
-        self.content_models = list(content_app.models.values())
+        self.content_models = list(content_app.get_models(include_auto_created=True))
 
         # Get the next available tree_id in our database
         self.tree_id = self.find_unique_tree_id()
@@ -159,7 +166,12 @@ def table_import(self, model, row_mapper, table_mapper, unflushed_rows):
         except ClassNotFoundError:
             SourceRecord = None
 
-        columns = dest_table.columns.keys()
+        # Filter out columns that are auto-incrementing integer primary keys, as these can cause collisions in the
+        # database. As all of our content database models use UUID primary keys, the only tables using these
+        # primary keys are intermediary tables for ManyToMany fields, and so nothing should be Foreign Keying
+        # to these ids.
+        # By filtering them here, the database should autoset an incremented id.
+        columns = [column_name for column_name, column_obj in dest_table.columns.items() if column_not_auto_integer_pk(column_obj)]
         data_to_insert = []
         merge = model in merge_models
         for record in table_mapper(SourceRecord):
@@ -189,13 +201,20 @@ def import_channel_data(self):
 
         unflushed_rows = 0
 
-        for model in self.content_models:
-            mapping = self.schema_mapping.get(model, {})
-            row_mapper = self.generate_row_mapper(mapping.get('per_row'))
-            table_mapper = self.generate_table_mapper(mapping.get('per_table'))
-            logging.info('Importing {model} data'.format(model=model.__name__))
-            unflushed_rows = self.table_import(model, row_mapper, table_mapper, unflushed_rows)
-        self.destination.session.commit()
+        try:
+            for model in self.content_models:
+                mapping = self.schema_mapping.get(model, {})
+                row_mapper = self.generate_row_mapper(mapping.get('per_row'))
+                table_mapper = self.generate_table_mapper(mapping.get('per_table'))
+                logging.info('Importing {model} data'.format(model=model.__name__))
+                unflushed_rows = self.table_import(model, row_mapper, table_mapper, unflushed_rows)
+            self.destination.session.commit()
+
+        except SQLAlchemyError as e:
+            # Rollback the transaction if any error occurs during the transaction
+            self.destination.session.rollback()
+            # Reraise the exception to prevent other errors occuring due to the non-completion
+            raise e
 
     def end(self):
         self.source.end()
diff --git a/kolibri/utils/version.py b/kolibri/utils/version.py
--- a/kolibri/utils/version.py
+++ b/kolibri/utils/version.py
@@ -4,8 +4,8 @@
 internal string representation in Python, you will find a
 `PEP-440 <https://www.python.org/dev/peps/pep-0440/>`__ flavor.
 
- * ``1.1.0`` (Semver)  = ``1.1`` (PEP-440).
- * ``1.0.0-alpha.1`` (Semver)  = ``1.0a1`` (PEP-440).
+ * ``1.1.0`` (Semver)  = ``1.1.0`` (PEP-440).
+ * ``1.0.0-alpha.1`` (Semver)  = ``1.0.0a1`` (PEP-440).
 
 Here's how version numbers are generated:
 
@@ -35,10 +35,10 @@
 +--------------+---------------------+---------------------+---------------------------+-----------------------------------+
 | Release type | ``kolibri.VERSION`` | ``kolibri/VERSION`` | Git data                  | Examples                          |
 +==============+=====================+=====================+===========================+===================================+
-| Final        | Canonical, only     | N/A                 | N/A                       | 0.1, 0.2.2,                       |
+| Final        | Canonical, only     | N/A                 | N/A                       | 0.1.0, 0.2.2,                       |
 |              | information used    |                     |                           | 0.2.post1                         |
 +--------------+---------------------+---------------------+---------------------------+-----------------------------------+
-| dev release  | (1, 2, 3, 'alpha',  | Fallback            | timestamp of latest       | 0.4.dev020170605181124-f1234567   |
+| dev release  | (1, 2, 3, 'alpha',  | Fallback            | timestamp of latest       | 0.4.0.dev020170605181124-f1234567   |
 | (alpha0)     | 0), 0th alpha = a   |                     | commit + hash             |                                   |
 |              | dev release! Never  |                     |                           |                                   |
 |              | used as a canonical |                     |                           |                                   |
@@ -62,11 +62,11 @@
 |              |                     |                     |                           | since tag:                        |
 |              |                     |                     |                           | 1.2.3rc1.dev123-f1234567          |
 +--------------+---------------------+---------------------+---------------------------+-----------------------------------+
-| beta0, rc0,  | Not recommended,    | Fallback            | timestamp of latest       | 0.4b0.dev020170605181124-f1234567 |
+| beta0, rc0,  | Not recommended,    | Fallback            | timestamp of latest       | 0.4.0b0.dev020170605181124-f1234567 |
 | post0, x.y.0 | but if you use it,  |                     | commit + hash             |                                   |
 |              | your release        |                     |                           |                                   |
 |              | transforms into a   |                     |                           |                                   |
-|              | X.Yb0.dev{suffix}   |                     |                           |                                   |
+|              | X.Y.0b0.dev{suffix} |                     |                           |                                   |
 |              | release, which in   |                     |                           |                                   |
 |              | most cases should   |                     |                           |                                   |
 |              | be assigned to the  |                     |                           |                                   |
@@ -157,11 +157,10 @@
 
 def get_major_version(version=None):
     """
-    :returns: String w/ first digit part of version tuple, either x.y or x.y.z
+    :returns: String w/ first digit part of version tuple x.y.z
     """
     version = get_complete_version(version)
-    parts = 2 if version[2] == 0 else 3
-    major = '.'.join(str(x) for x in version[:parts])
+    major = '.'.join(str(x) for x in version[:3])
     return major
 
 
@@ -400,7 +399,7 @@ def get_version(version=None):
     This is important to read from PEP-404 (which this function is compliant
     with):
 
-    Within a numeric release ( 1.0 , 2.7.3 ), the following suffixes are
+    Within a numeric release ( 1.0.0 , 2.7.3 ), the following suffixes are
     permitted and MUST be ordered as shown:
 
     .devN, aN, bN, rcN, <no suffix>, .postN


Test patch:
diff --git a/kolibri/content/test/test_channel_import.py b/kolibri/content/test/test_channel_import.py
--- a/kolibri/content/test/test_channel_import.py
+++ b/kolibri/content/test/test_channel_import.py
@@ -38,7 +38,7 @@ def test_two_bridges(self, db_path_mock, apps_mock, tree_id_mock, BridgeMock):
     @patch('kolibri.content.utils.channel_import.get_content_database_file_path')
     def test_get_config(self, db_path_mock, apps_mock, tree_id_mock, BridgeMock):
         ChannelImport('test')
-        apps_mock.assert_has_calls([call.get_app_config('content'), call.get_app_config().models.values()])
+        apps_mock.assert_has_calls([call.get_app_config('content'), call.get_app_config().get_models(include_auto_created=True)])
 
     def test_tree_id(self, apps_mock, tree_id_mock, BridgeMock):
         ChannelImport('test')
@@ -191,7 +191,7 @@ def test_no_models_unflushed_rows_passed_through(self, apps_mock, tree_id_mock,
     def test_no_merge_records_bulk_insert_no_flush(self, apps_mock, tree_id_mock, BridgeMock):
         channel_import = ChannelImport('test')
         record_mock = MagicMock(spec=['__table__'])
-        record_mock.__table__.columns.keys.return_value = ['test_attr']
+        record_mock.__table__.columns.items.return_value = [('test_attr', MagicMock())]
         channel_import.destination.get_class.return_value = record_mock
         channel_import.table_import(MagicMock(), lambda x, y: 'test_val', lambda x: [{}]*100, 0)
         channel_import.destination.session.bulk_insert_mappings.assert_has_calls([call(record_mock, [{'test_attr': 'test_val'}]*100)])
@@ -200,7 +200,7 @@ def test_no_merge_records_bulk_insert_no_flush(self, apps_mock, tree_id_mock, Br
     def test_no_merge_records_bulk_insert_flush(self, apps_mock, tree_id_mock, BridgeMock):
         channel_import = ChannelImport('test')
         record_mock = MagicMock(spec=['__table__'])
-        record_mock.__table__.columns.keys.return_value = ['test_attr']
+        record_mock.__table__.columns.items.return_value = [('test_attr', MagicMock())]
         channel_import.destination.get_class.return_value = record_mock
         channel_import.table_import(MagicMock(), lambda x, y: 'test_val', lambda x: [{}]*10000, 0)
         channel_import.destination.session.bulk_insert_mappings.assert_has_calls([call(record_mock, [{'test_attr': 'test_val'}]*10000)])
@@ -211,7 +211,7 @@ def test_merge_records_merge_no_flush(self, apps_mock, tree_id_mock, BridgeMock)
         from kolibri.content.utils.channel_import import merge_models
         channel_import = ChannelImport('test')
         record_mock = MagicMock(spec=['__table__'])
-        record_mock.__table__.columns.keys.return_value = ['test_attr']
+        record_mock.__table__.columns.items.return_value = [('test_attr', MagicMock())]
         channel_import.destination.get_class.return_value = record_mock
         model_mock = MagicMock()
         merge_models.append(model_mock)
@@ -224,7 +224,7 @@ def test_merge_records_merge_flush(self, apps_mock, tree_id_mock, BridgeMock):
         from kolibri.content.utils.channel_import import merge_models
         channel_import = ChannelImport('test')
         record_mock = Mock(spec=['__table__'])
-        record_mock.__table__.columns.keys.return_value = ['test_attr']
+        record_mock.__table__.columns.items.return_value = [('test_attr', MagicMock())]
         channel_import.destination.get_class.return_value = record_mock
         model_mock = Mock()
         merge_models.append(model_mock)
diff --git a/kolibri/utils/tests/test_version.py b/kolibri/utils/tests/test_version.py
--- a/kolibri/utils/tests/test_version.py
+++ b/kolibri/utils/tests/test_version.py
@@ -52,7 +52,7 @@ def test_alpha_0_version(self):
         hickups with ``git describe --tag``.
         """
         v = get_version((0, 1, 0, "alpha", 0))
-        self.assertIn("0.1.dev", v)
+        self.assertIn("0.1.0.dev", v)
 
     def test_alpha_1_version(self):
         """
@@ -65,7 +65,7 @@ def test_alpha_1_version(self):
         version.assert_git_version = lambda *x: True
         try:
             v = get_version((0, 1, 0, "alpha", 1))
-            self.assertIn("0.1a1", v)
+            self.assertIn("0.1.0a1", v)
         finally:
             version.assert_git_version = assert_version
 
@@ -79,7 +79,7 @@ def test_alpha_1_version_no_git(self):
         version.get_version_file = lambda: None
         try:
             v = get_version((0, 1, 0, "alpha", 1))
-            self.assertIn("0.1a1", v)
+            self.assertIn("0.1.0a1", v)
         finally:
             version.get_version_file = get_version_file
 
@@ -90,10 +90,10 @@ def test_alpha_1_version_file(self):
         """
         # Simple mocking
         get_version_file = version.get_version_file
-        version.get_version_file = lambda: "0.1a1"
+        version.get_version_file = lambda: "0.1.0a1"
         try:
             v = get_version((0, 1, 0, "alpha", 1))
-            self.assertIn("0.1a1", v)
+            self.assertIn("0.1.0a1", v)
         finally:
             version.get_version_file = get_version_file
 
@@ -106,10 +106,10 @@ def test_version_file_linebreaks(self):
         """
         # Simple mocking
         get_version_file = version.get_version_file
-        version.get_version_file = lambda: "0.1a1\n"
+        version.get_version_file = lambda: "0.1.0a1\n"
         try:
             v = get_version((0, 1, 0, "alpha", 1))
-            self.assertIn("0.1a1", v)
+            self.assertIn("0.1.0a1", v)
         finally:
             version.get_version_file = get_version_file
 
@@ -120,7 +120,7 @@ def test_alpha_1_inconsistent_version_file(self):
         """
         # Simple mocking
         get_version_file = version.get_version_file
-        version.get_version_file = lambda: "0.2a1"
+        version.get_version_file = lambda: "0.2.0a1"
         try:
             self.assertRaises(
                 AssertionError,
@@ -164,7 +164,7 @@ def test_final(self):
         Test that the major version is set as expected on a final release
         """
         v = get_version((0, 1, 0, "final", 0))
-        self.assertEqual(v, "0.1")
+        self.assertEqual(v, "0.1.0")
 
     def test_final_patch(self):
         """