prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Numbers should be safe characters in file names
  If the attachment for the "Exercise with attachment" is named for example "2.zip", the name for that file will be just ".zip" in the database because numbers are not considered as safe characters:
  
  https://github.com/Aalto-LeTech/a-plus/blob/a86bf/lib/helpers.py#L62
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `lib/helpers.py`
  Content:
  ```
  1 from django.conf import settings
  2 from random import choice
  3 from PIL import Image
  4 import string
  5 import urllib
  6 
  7 
  8 def extract_form_errors(form):
  9     """
  10     Extracts Django form errors to a list of error messages.
  11     """
  12     errors = []
  13     for field in form.errors:
  14         for err in form.errors[field]:
  15             errors.append("%s: %s" % (field, err))
  16     return errors
  17 
  18 
  19 def get_random_string(length=32):
  20     """
  21     This function creates a random string with a given length.
  22     The strings consist of upper and lower case letters and numbers.
  23 
  24     @param length: the length of the randomized string, defaults to 32
  25     @return: a random string containing lower and upper case letters and digits
  26     """
  27 
  28     # Use all letters and numbers in the identifier
  29     choices = string.ascii_letters + string.digits
  30 
  31     return ''.join([choice(choices) for _ in range(length)])
  32 
  33 
  34 def query_dict_to_list_of_tuples(query_dict):
  35     """
  36     This helper function creates a list of tuples with the values
  37     from a QueryDict object. In a QueryDict the same key can have
  38     several values, which is not possible with a typical dict nor a JSON
  39     object. The resulting list will be similar to [(key1, value1), (key2, value2)].
  40 
  41     @param query_dict: a QueryDict object
  42     @return: a list of tuples with the same keys and values as in the given QueryDict
  43     """
  44     list_of_tuples = []
  45     for key in query_dict:
  46         for val in query_dict.getlist(key):
  47             list_of_tuples.append((key, val))
  48     return list_of_tuples
  49 
  50 
  51 def update_url_params(url, params):
  52     delimiter = "&" if "?" in url else "?"
  53     return url + delimiter + urllib.parse.urlencode(params)
  54 
  55 
  56 def has_same_domain(url1, url2):
  57     uri1 = urllib.parse.urlparse(url1)
  58     uri2 = urllib.parse.urlparse(url2)
  59     return uri1.netloc == uri2.netloc
  60 
  61 
  62 FILENAME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-"
  63 
  64 def safe_file_name(name):
  65     safename = "".join(c for c in name if c in FILENAME_CHARS)
  66     if safename[0] == "-":
  67         return "_" + safename[1:80]
  68     return safename[:80]
  69 
  70 
  71 def resize_image(path, max_size):
  72     image = Image.open(path)
  73     image.thumbnail(max_size, Image.ANTIALIAS)
  74     image.save(path)
  75 
  76 
  77 def roman_numeral(number):
  78     numbers = [1000,900,500,400,100,90,50,40,10,9,5,4,1];
  79     letters = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"];
  80     roman = ""
  81     for i in range(len(numbers)):
  82         while number >= numbers[i]:
  83             roman += letters[i]
  84             number -= numbers[i]
  85     return roman
  86 
  87 
  88 def settings_text(request, key):
  89     def get(name):
  90         if hasattr(settings, name):
  91             return getattr(settings, name)
  92         return None
  93     return get('{}_{}'.format(key, request.LANGUAGE_CODE.upper())) or get(key)
  94
  ```
  --- 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/lib/helpers.py b/lib/helpers.py
  --- a/lib/helpers.py
  +++ b/lib/helpers.py
  @@ -59,7 +59,7 @@
       return uri1.netloc == uri2.netloc
   
   
  -FILENAME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-"
  +FILENAME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-0123456789"
   
   def safe_file_name(name):
       safename = "".join(c for c in name if c in FILENAME_CHARS)
  
verification_info: {"golden_diff": "diff --git a/lib/helpers.py b/lib/helpers.py\n--- a/lib/helpers.py\n+++ b/lib/helpers.py\n@@ -59,7 +59,7 @@\n     return uri1.netloc == uri2.netloc\n \n \n-FILENAME_CHARS = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-\"\n+FILENAME_CHARS = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-0123456789\"\n \n def safe_file_name(name):\n     safename = \"\".join(c for c in name if c in FILENAME_CHARS)\n", "issue": "Numbers should be safe characters in file names\nIf the attachment for the \"Exercise with attachment\" is named for example \"2.zip\", the name for that file will be just \".zip\" in the database because numbers are not considered as safe characters:\n\nhttps://github.com/Aalto-LeTech/a-plus/blob/a86bf/lib/helpers.py#L62\n\n", "before_files": [{"content": "from django.conf import settings\nfrom random import choice\nfrom PIL import Image\nimport string\nimport urllib\n\n\ndef extract_form_errors(form):\n    \"\"\"\n    Extracts Django form errors to a list of error messages.\n    \"\"\"\n    errors = []\n    for field in form.errors:\n        for err in form.errors[field]:\n            errors.append(\"%s: %s\" % (field, err))\n    return errors\n\n\ndef get_random_string(length=32):\n    \"\"\"\n    This function creates a random string with a given length.\n    The strings consist of upper and lower case letters and numbers.\n\n    @param length: the length of the randomized string, defaults to 32\n    @return: a random string containing lower and upper case letters and digits\n    \"\"\"\n\n    # Use all letters and numbers in the identifier\n    choices = string.ascii_letters + string.digits\n\n    return ''.join([choice(choices) for _ in range(length)])\n\n\ndef query_dict_to_list_of_tuples(query_dict):\n    \"\"\"\n    This helper function creates a list of tuples with the values\n    from a QueryDict object. In a QueryDict the same key can have\n    several values, which is not possible with a typical dict nor a JSON\n    object. The resulting list will be similar to [(key1, value1), (key2, value2)].\n\n    @param query_dict: a QueryDict object\n    @return: a list of tuples with the same keys and values as in the given QueryDict\n    \"\"\"\n    list_of_tuples = []\n    for key in query_dict:\n        for val in query_dict.getlist(key):\n            list_of_tuples.append((key, val))\n    return list_of_tuples\n\n\ndef update_url_params(url, params):\n    delimiter = \"&\" if \"?\" in url else \"?\"\n    return url + delimiter + urllib.parse.urlencode(params)\n\n\ndef has_same_domain(url1, url2):\n    uri1 = urllib.parse.urlparse(url1)\n    uri2 = urllib.parse.urlparse(url2)\n    return uri1.netloc == uri2.netloc\n\n\nFILENAME_CHARS = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-\"\n\ndef safe_file_name(name):\n    safename = \"\".join(c for c in name if c in FILENAME_CHARS)\n    if safename[0] == \"-\":\n        return \"_\" + safename[1:80]\n    return safename[:80]\n\n\ndef resize_image(path, max_size):\n    image = Image.open(path)\n    image.thumbnail(max_size, Image.ANTIALIAS)\n    image.save(path)\n\n\ndef roman_numeral(number):\n    numbers = [1000,900,500,400,100,90,50,40,10,9,5,4,1];\n    letters = [\"M\",\"CM\",\"D\",\"CD\",\"C\",\"XC\",\"L\",\"XL\",\"X\",\"IX\",\"V\",\"IV\",\"I\"];\n    roman = \"\"\n    for i in range(len(numbers)):\n        while number >= numbers[i]:\n            roman += letters[i]\n            number -= numbers[i]\n    return roman\n\n\ndef settings_text(request, key):\n    def get(name):\n        if hasattr(settings, name):\n            return getattr(settings, name)\n        return None\n    return get('{}_{}'.format(key, request.LANGUAGE_CODE.upper())) or get(key)\n", "path": "lib/helpers.py"}], "after_files": [{"content": "from django.conf import settings\nfrom random import choice\nfrom PIL import Image\nimport string\nimport urllib\n\n\ndef extract_form_errors(form):\n    \"\"\"\n    Extracts Django form errors to a list of error messages.\n    \"\"\"\n    errors = []\n    for field in form.errors:\n        for err in form.errors[field]:\n            errors.append(\"%s: %s\" % (field, err))\n    return errors\n\n\ndef get_random_string(length=32):\n    \"\"\"\n    This function creates a random string with a given length.\n    The strings consist of upper and lower case letters and numbers.\n\n    @param length: the length of the randomized string, defaults to 32\n    @return: a random string containing lower and upper case letters and digits\n    \"\"\"\n\n    # Use all letters and numbers in the identifier\n    choices = string.ascii_letters + string.digits\n\n    return ''.join([choice(choices) for _ in range(length)])\n\n\ndef query_dict_to_list_of_tuples(query_dict):\n    \"\"\"\n    This helper function creates a list of tuples with the values\n    from a QueryDict object. In a QueryDict the same key can have\n    several values, which is not possible with a typical dict nor a JSON\n    object. The resulting list will be similar to [(key1, value1), (key2, value2)].\n\n    @param query_dict: a QueryDict object\n    @return: a list of tuples with the same keys and values as in the given QueryDict\n    \"\"\"\n    list_of_tuples = []\n    for key in query_dict:\n        for val in query_dict.getlist(key):\n            list_of_tuples.append((key, val))\n    return list_of_tuples\n\n\ndef update_url_params(url, params):\n    delimiter = \"&\" if \"?\" in url else \"?\"\n    return url + delimiter + urllib.parse.urlencode(params)\n\n\ndef has_same_domain(url1, url2):\n    uri1 = urllib.parse.urlparse(url1)\n    uri2 = urllib.parse.urlparse(url2)\n    return uri1.netloc == uri2.netloc\n\n\nFILENAME_CHARS = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-0123456789\"\n\ndef safe_file_name(name):\n    safename = \"\".join(c for c in name if c in FILENAME_CHARS)\n    if safename[0] == \"-\":\n        return \"_\" + safename[1:80]\n    return safename[:80]\n\n\ndef resize_image(path, max_size):\n    image = Image.open(path)\n    image.thumbnail(max_size, Image.ANTIALIAS)\n    image.save(path)\n\n\ndef roman_numeral(number):\n    numbers = [1000,900,500,400,100,90,50,40,10,9,5,4,1];\n    letters = [\"M\",\"CM\",\"D\",\"CD\",\"C\",\"XC\",\"L\",\"XL\",\"X\",\"IX\",\"V\",\"IV\",\"I\"];\n    roman = \"\"\n    for i in range(len(numbers)):\n        while number >= numbers[i]:\n            roman += letters[i]\n            number -= numbers[i]\n    return roman\n\n\ndef settings_text(request, key):\n    def get(name):\n        if hasattr(settings, name):\n            return getattr(settings, name)\n        return None\n    return get('{}_{}'.format(key, request.LANGUAGE_CODE.upper())) or get(key)\n", "path": "lib/helpers.py"}]}