prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  import footer styling regression
  
  
  ### Observed behavior
  
  ![image](https://user-images.githubusercontent.com/2367265/85073073-fef1cd80-b16e-11ea-98c5-8e2342430929.png)
  
  ### Expected behavior
  
  button and text should be vertically centered, or the footer should be shorter in height
  
  
  
  ### Steps to reproduce
  
  import
  
  ### Context
  
  
  0.14 beta 3
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `kolibri/core/content/utils/channels.py`
  Content:
  ```
  1 import fnmatch
  2 import logging
  3 import os
  4 
  5 from django.core.cache import cache
  6 from sqlalchemy.exc import DatabaseError
  7 
  8 from .paths import get_content_database_dir_path
  9 from .sqlalchemybridge import Bridge
  10 from kolibri.core.discovery.utils.filesystem import enumerate_mounted_disk_partitions
  11 from kolibri.utils.uuids import is_valid_uuid
  12 
  13 logger = logging.getLogger(__name__)
  14 
  15 
  16 def get_channel_ids_for_content_dirs(content_dirs):
  17     database_dir_paths = [
  18         get_content_database_dir_path(contentfolder=path) for path in content_dirs
  19     ]
  20     channel_ids = set()
  21     for path in database_dir_paths:
  22         channel_ids.update(get_channel_ids_for_content_database_dir(path))
  23     return list(channel_ids)
  24 
  25 
  26 def get_channel_ids_for_content_database_dir(content_database_dir):
  27     """
  28     Returns a list of channel IDs for the channel databases that exist in a content database directory.
  29     """
  30 
  31     # immediately return an empty list if the content database directory doesn't exist
  32     if not os.path.isdir(content_database_dir):
  33         return []
  34 
  35     # get a list of all the database files in the directory, and extract IDs
  36     db_list = fnmatch.filter(os.listdir(content_database_dir), "*.sqlite3")
  37     db_names = [db.split(".sqlite3", 1)[0] for db in db_list]
  38 
  39     # determine which database names are valid, and only use those ones
  40     valid_db_names = [name for name in db_names if is_valid_uuid(name)]
  41     invalid_db_names = set(db_names) - set(valid_db_names)
  42     if invalid_db_names:
  43         logger.warning(
  44             "Ignoring databases in content database directory '{directory}' with invalid names: {names}".format(
  45                 directory=content_database_dir, names=invalid_db_names
  46             )
  47         )
  48 
  49     # nonexistent database files are created if we delete the files that have broken symbolic links;
  50     # empty database files are created if we delete a database file while the server is running and connected to it;
  51     # here, we delete and exclude such databases to avoid errors when we try to connect to them
  52     db_files_to_remove = set({})
  53     for db_name in valid_db_names:
  54         filename = os.path.join(content_database_dir, "{}.sqlite3".format(db_name))
  55         if not os.path.exists(filename) or os.path.getsize(filename) == 0:
  56             db_files_to_remove.add(db_name)
  57             os.remove(filename)
  58 
  59     if db_files_to_remove:
  60         err_msg = (
  61             "Removing nonexistent or empty databases in content database directory "
  62             "'{directory}' with IDs: {names}.\nPlease import the channels again."
  63         )
  64         logger.warning(
  65             err_msg.format(directory=content_database_dir, names=db_files_to_remove)
  66         )
  67     valid_dbs = list(set(valid_db_names) - set(db_files_to_remove))
  68 
  69     return valid_dbs
  70 
  71 
  72 def enumerate_content_database_file_paths(content_database_dir):
  73     full_dir_template = os.path.join(content_database_dir, "{}.sqlite3")
  74     channel_ids = get_channel_ids_for_content_database_dir(content_database_dir)
  75     return [full_dir_template.format(f) for f in channel_ids]
  76 
  77 
  78 def read_channel_metadata_from_db_file(channeldbpath):
  79     # import here to avoid circular imports whenever kolibri.core.content.models imports utils too
  80     from kolibri.core.content.models import ChannelMetadata
  81 
  82     source = Bridge(sqlite_file_path=channeldbpath)
  83 
  84     ChannelMetadataClass = source.get_class(ChannelMetadata)
  85 
  86     source_channel_metadata = source.session.query(ChannelMetadataClass).all()[0]
  87 
  88     # Use the inferred version from the SQLAlchemy Bridge object, and set it as additional
  89     # metadata on the channel data
  90 
  91     source_channel_metadata.inferred_schema_version = source.schema_version
  92 
  93     source.end()
  94 
  95     # Adds an attribute `root_id` when `root_id` does not exist to match with
  96     # the latest schema.
  97     if not hasattr(source_channel_metadata, "root_id"):
  98         setattr(
  99             source_channel_metadata,
  100             "root_id",
  101             getattr(source_channel_metadata, "root_pk"),
  102         )
  103 
  104     return source_channel_metadata
  105 
  106 
  107 def get_channels_for_data_folder(datafolder):
  108     channels = []
  109     for path in enumerate_content_database_file_paths(
  110         get_content_database_dir_path(datafolder)
  111     ):
  112         try:
  113             channel = read_channel_metadata_from_db_file(path)
  114         except DatabaseError:
  115             logger.warning(
  116                 "Tried to import channel from database file {}, but the file was corrupted.".format(
  117                     path
  118                 )
  119             )
  120             continue
  121         channel_data = {
  122             "path": path,
  123             "id": channel.id,
  124             "name": channel.name,
  125             "description": channel.description,
  126             "tagline": channel.tagline,
  127             "thumbnail": channel.thumbnail,
  128             "version": channel.version,
  129             "root": channel.root_id,
  130             "author": channel.author,
  131             "last_updated": getattr(channel, "last_updated", None),
  132             "lang_code": getattr(channel, "lang_code", None),
  133             "lang_name": getattr(channel, "lang_name", None),
  134         }
  135         channels.append(channel_data)
  136     return channels
  137 
  138 
  139 # Use this to cache mounted drive information when
  140 # it has already been fetched for querying by drive id
  141 MOUNTED_DRIVES_CACHE_KEY = "mounted_drives_cache_key"
  142 
  143 
  144 def get_mounted_drives_with_channel_info():
  145     drives = enumerate_mounted_disk_partitions()
  146     for drive in drives.values():
  147         drive.metadata["channels"] = (
  148             get_channels_for_data_folder(drive.datafolder) if drive.datafolder else []
  149         )
  150     cache.set(MOUNTED_DRIVES_CACHE_KEY, drives, 3600)
  151     return drives
  152 
  153 
  154 def get_mounted_drive_by_id(drive_id):
  155     drives = cache.get(MOUNTED_DRIVES_CACHE_KEY)
  156     if drives is None or drives.get(drive_id, None) is None:
  157         drives = get_mounted_drives_with_channel_info()
  158     return drives[drive_id]
  159
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/kolibri/core/content/utils/channels.py b/kolibri/core/content/utils/channels.py
  --- a/kolibri/core/content/utils/channels.py
  +++ b/kolibri/core/content/utils/channels.py
  @@ -123,7 +123,7 @@
               "id": channel.id,
               "name": channel.name,
               "description": channel.description,
  -            "tagline": channel.tagline,
  +            "tagline": getattr(channel, "tagline", ""),
               "thumbnail": channel.thumbnail,
               "version": channel.version,
               "root": channel.root_id,
  
verification_info: {"golden_diff": "diff --git a/kolibri/core/content/utils/channels.py b/kolibri/core/content/utils/channels.py\n--- a/kolibri/core/content/utils/channels.py\n+++ b/kolibri/core/content/utils/channels.py\n@@ -123,7 +123,7 @@\n             \"id\": channel.id,\n             \"name\": channel.name,\n             \"description\": channel.description,\n-            \"tagline\": channel.tagline,\n+            \"tagline\": getattr(channel, \"tagline\", \"\"),\n             \"thumbnail\": channel.thumbnail,\n             \"version\": channel.version,\n             \"root\": channel.root_id,\n", "issue": "import footer styling regression\n\r\n\r\n### Observed behavior\r\n\r\n![image](https://user-images.githubusercontent.com/2367265/85073073-fef1cd80-b16e-11ea-98c5-8e2342430929.png)\r\n\r\n### Expected behavior\r\n\r\nbutton and text should be vertically centered, or the footer should be shorter in height\r\n\r\n\r\n\r\n### Steps to reproduce\r\n\r\nimport\r\n\r\n### Context\r\n\r\n\r\n0.14 beta 3\n", "before_files": [{"content": "import fnmatch\nimport logging\nimport os\n\nfrom django.core.cache import cache\nfrom sqlalchemy.exc import DatabaseError\n\nfrom .paths import get_content_database_dir_path\nfrom .sqlalchemybridge import Bridge\nfrom kolibri.core.discovery.utils.filesystem import enumerate_mounted_disk_partitions\nfrom kolibri.utils.uuids import is_valid_uuid\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_channel_ids_for_content_dirs(content_dirs):\n    database_dir_paths = [\n        get_content_database_dir_path(contentfolder=path) for path in content_dirs\n    ]\n    channel_ids = set()\n    for path in database_dir_paths:\n        channel_ids.update(get_channel_ids_for_content_database_dir(path))\n    return list(channel_ids)\n\n\ndef get_channel_ids_for_content_database_dir(content_database_dir):\n    \"\"\"\n    Returns a list of channel IDs for the channel databases that exist in a content database directory.\n    \"\"\"\n\n    # immediately return an empty list if the content database directory doesn't exist\n    if not os.path.isdir(content_database_dir):\n        return []\n\n    # get a list of all the database files in the directory, and extract IDs\n    db_list = fnmatch.filter(os.listdir(content_database_dir), \"*.sqlite3\")\n    db_names = [db.split(\".sqlite3\", 1)[0] for db in db_list]\n\n    # determine which database names are valid, and only use those ones\n    valid_db_names = [name for name in db_names if is_valid_uuid(name)]\n    invalid_db_names = set(db_names) - set(valid_db_names)\n    if invalid_db_names:\n        logger.warning(\n            \"Ignoring databases in content database directory '{directory}' with invalid names: {names}\".format(\n                directory=content_database_dir, names=invalid_db_names\n            )\n        )\n\n    # nonexistent database files are created if we delete the files that have broken symbolic links;\n    # empty database files are created if we delete a database file while the server is running and connected to it;\n    # here, we delete and exclude such databases to avoid errors when we try to connect to them\n    db_files_to_remove = set({})\n    for db_name in valid_db_names:\n        filename = os.path.join(content_database_dir, \"{}.sqlite3\".format(db_name))\n        if not os.path.exists(filename) or os.path.getsize(filename) == 0:\n            db_files_to_remove.add(db_name)\n            os.remove(filename)\n\n    if db_files_to_remove:\n        err_msg = (\n            \"Removing nonexistent or empty databases in content database directory \"\n            \"'{directory}' with IDs: {names}.\\nPlease import the channels again.\"\n        )\n        logger.warning(\n            err_msg.format(directory=content_database_dir, names=db_files_to_remove)\n        )\n    valid_dbs = list(set(valid_db_names) - set(db_files_to_remove))\n\n    return valid_dbs\n\n\ndef enumerate_content_database_file_paths(content_database_dir):\n    full_dir_template = os.path.join(content_database_dir, \"{}.sqlite3\")\n    channel_ids = get_channel_ids_for_content_database_dir(content_database_dir)\n    return [full_dir_template.format(f) for f in channel_ids]\n\n\ndef read_channel_metadata_from_db_file(channeldbpath):\n    # import here to avoid circular imports whenever kolibri.core.content.models imports utils too\n    from kolibri.core.content.models import ChannelMetadata\n\n    source = Bridge(sqlite_file_path=channeldbpath)\n\n    ChannelMetadataClass = source.get_class(ChannelMetadata)\n\n    source_channel_metadata = source.session.query(ChannelMetadataClass).all()[0]\n\n    # Use the inferred version from the SQLAlchemy Bridge object, and set it as additional\n    # metadata on the channel data\n\n    source_channel_metadata.inferred_schema_version = source.schema_version\n\n    source.end()\n\n    # Adds an attribute `root_id` when `root_id` does not exist to match with\n    # the latest schema.\n    if not hasattr(source_channel_metadata, \"root_id\"):\n        setattr(\n            source_channel_metadata,\n            \"root_id\",\n            getattr(source_channel_metadata, \"root_pk\"),\n        )\n\n    return source_channel_metadata\n\n\ndef get_channels_for_data_folder(datafolder):\n    channels = []\n    for path in enumerate_content_database_file_paths(\n        get_content_database_dir_path(datafolder)\n    ):\n        try:\n            channel = read_channel_metadata_from_db_file(path)\n        except DatabaseError:\n            logger.warning(\n                \"Tried to import channel from database file {}, but the file was corrupted.\".format(\n                    path\n                )\n            )\n            continue\n        channel_data = {\n            \"path\": path,\n            \"id\": channel.id,\n            \"name\": channel.name,\n            \"description\": channel.description,\n            \"tagline\": channel.tagline,\n            \"thumbnail\": channel.thumbnail,\n            \"version\": channel.version,\n            \"root\": channel.root_id,\n            \"author\": channel.author,\n            \"last_updated\": getattr(channel, \"last_updated\", None),\n            \"lang_code\": getattr(channel, \"lang_code\", None),\n            \"lang_name\": getattr(channel, \"lang_name\", None),\n        }\n        channels.append(channel_data)\n    return channels\n\n\n# Use this to cache mounted drive information when\n# it has already been fetched for querying by drive id\nMOUNTED_DRIVES_CACHE_KEY = \"mounted_drives_cache_key\"\n\n\ndef get_mounted_drives_with_channel_info():\n    drives = enumerate_mounted_disk_partitions()\n    for drive in drives.values():\n        drive.metadata[\"channels\"] = (\n            get_channels_for_data_folder(drive.datafolder) if drive.datafolder else []\n        )\n    cache.set(MOUNTED_DRIVES_CACHE_KEY, drives, 3600)\n    return drives\n\n\ndef get_mounted_drive_by_id(drive_id):\n    drives = cache.get(MOUNTED_DRIVES_CACHE_KEY)\n    if drives is None or drives.get(drive_id, None) is None:\n        drives = get_mounted_drives_with_channel_info()\n    return drives[drive_id]\n", "path": "kolibri/core/content/utils/channels.py"}], "after_files": [{"content": "import fnmatch\nimport logging\nimport os\n\nfrom django.core.cache import cache\nfrom sqlalchemy.exc import DatabaseError\n\nfrom .paths import get_content_database_dir_path\nfrom .sqlalchemybridge import Bridge\nfrom kolibri.core.discovery.utils.filesystem import enumerate_mounted_disk_partitions\nfrom kolibri.utils.uuids import is_valid_uuid\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_channel_ids_for_content_dirs(content_dirs):\n    database_dir_paths = [\n        get_content_database_dir_path(contentfolder=path) for path in content_dirs\n    ]\n    channel_ids = set()\n    for path in database_dir_paths:\n        channel_ids.update(get_channel_ids_for_content_database_dir(path))\n    return list(channel_ids)\n\n\ndef get_channel_ids_for_content_database_dir(content_database_dir):\n    \"\"\"\n    Returns a list of channel IDs for the channel databases that exist in a content database directory.\n    \"\"\"\n\n    # immediately return an empty list if the content database directory doesn't exist\n    if not os.path.isdir(content_database_dir):\n        return []\n\n    # get a list of all the database files in the directory, and extract IDs\n    db_list = fnmatch.filter(os.listdir(content_database_dir), \"*.sqlite3\")\n    db_names = [db.split(\".sqlite3\", 1)[0] for db in db_list]\n\n    # determine which database names are valid, and only use those ones\n    valid_db_names = [name for name in db_names if is_valid_uuid(name)]\n    invalid_db_names = set(db_names) - set(valid_db_names)\n    if invalid_db_names:\n        logger.warning(\n            \"Ignoring databases in content database directory '{directory}' with invalid names: {names}\".format(\n                directory=content_database_dir, names=invalid_db_names\n            )\n        )\n\n    # nonexistent database files are created if we delete the files that have broken symbolic links;\n    # empty database files are created if we delete a database file while the server is running and connected to it;\n    # here, we delete and exclude such databases to avoid errors when we try to connect to them\n    db_files_to_remove = set({})\n    for db_name in valid_db_names:\n        filename = os.path.join(content_database_dir, \"{}.sqlite3\".format(db_name))\n        if not os.path.exists(filename) or os.path.getsize(filename) == 0:\n            db_files_to_remove.add(db_name)\n            os.remove(filename)\n\n    if db_files_to_remove:\n        err_msg = (\n            \"Removing nonexistent or empty databases in content database directory \"\n            \"'{directory}' with IDs: {names}.\\nPlease import the channels again.\"\n        )\n        logger.warning(\n            err_msg.format(directory=content_database_dir, names=db_files_to_remove)\n        )\n    valid_dbs = list(set(valid_db_names) - set(db_files_to_remove))\n\n    return valid_dbs\n\n\ndef enumerate_content_database_file_paths(content_database_dir):\n    full_dir_template = os.path.join(content_database_dir, \"{}.sqlite3\")\n    channel_ids = get_channel_ids_for_content_database_dir(content_database_dir)\n    return [full_dir_template.format(f) for f in channel_ids]\n\n\ndef read_channel_metadata_from_db_file(channeldbpath):\n    # import here to avoid circular imports whenever kolibri.core.content.models imports utils too\n    from kolibri.core.content.models import ChannelMetadata\n\n    source = Bridge(sqlite_file_path=channeldbpath)\n\n    ChannelMetadataClass = source.get_class(ChannelMetadata)\n\n    source_channel_metadata = source.session.query(ChannelMetadataClass).all()[0]\n\n    # Use the inferred version from the SQLAlchemy Bridge object, and set it as additional\n    # metadata on the channel data\n\n    source_channel_metadata.inferred_schema_version = source.schema_version\n\n    source.end()\n\n    # Adds an attribute `root_id` when `root_id` does not exist to match with\n    # the latest schema.\n    if not hasattr(source_channel_metadata, \"root_id\"):\n        setattr(\n            source_channel_metadata,\n            \"root_id\",\n            getattr(source_channel_metadata, \"root_pk\"),\n        )\n\n    return source_channel_metadata\n\n\ndef get_channels_for_data_folder(datafolder):\n    channels = []\n    for path in enumerate_content_database_file_paths(\n        get_content_database_dir_path(datafolder)\n    ):\n        try:\n            channel = read_channel_metadata_from_db_file(path)\n        except DatabaseError:\n            logger.warning(\n                \"Tried to import channel from database file {}, but the file was corrupted.\".format(\n                    path\n                )\n            )\n            continue\n        channel_data = {\n            \"path\": path,\n            \"id\": channel.id,\n            \"name\": channel.name,\n            \"description\": channel.description,\n            \"tagline\": getattr(channel, \"tagline\", \"\"),\n            \"thumbnail\": channel.thumbnail,\n            \"version\": channel.version,\n            \"root\": channel.root_id,\n            \"author\": channel.author,\n            \"last_updated\": getattr(channel, \"last_updated\", None),\n            \"lang_code\": getattr(channel, \"lang_code\", None),\n            \"lang_name\": getattr(channel, \"lang_name\", None),\n        }\n        channels.append(channel_data)\n    return channels\n\n\n# Use this to cache mounted drive information when\n# it has already been fetched for querying by drive id\nMOUNTED_DRIVES_CACHE_KEY = \"mounted_drives_cache_key\"\n\n\ndef get_mounted_drives_with_channel_info():\n    drives = enumerate_mounted_disk_partitions()\n    for drive in drives.values():\n        drive.metadata[\"channels\"] = (\n            get_channels_for_data_folder(drive.datafolder) if drive.datafolder else []\n        )\n    cache.set(MOUNTED_DRIVES_CACHE_KEY, drives, 3600)\n    return drives\n\n\ndef get_mounted_drive_by_id(drive_id):\n    drives = cache.get(MOUNTED_DRIVES_CACHE_KEY)\n    if drives is None or drives.get(drive_id, None) is None:\n        drives = get_mounted_drives_with_channel_info()\n    return drives[drive_id]\n", "path": "kolibri/core/content/utils/channels.py"}]}