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()

```

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)

{"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"}]}