Code

Ticket #7614: 7814_cleanups.2.diff

File 7814_cleanups.2.diff, 25.0 KB (added by axiak, 6 years ago)

Overall cleanups for r7814...addressing all API changes that I know of.

Line 
1Index: django/oldforms/__init__.py
2===================================================================
3--- django/oldforms/__init__.py (revision 7827)
4+++ django/oldforms/__init__.py (working copy)
5@@ -686,7 +686,7 @@
6             if upload_errors:
7                 raise validators.CriticalValidationError, upload_errors
8         try:
9-            file_size = new_data.file_size
10+            file_size = new_data.size
11         except AttributeError:
12             file_size = len(new_data['content'])
13         if not file_size:
14Index: django/db/models/base.py
15===================================================================
16--- django/db/models/base.py    (revision 7827)
17+++ django/db/models/base.py    (working copy)
18@@ -540,7 +540,7 @@
19             # This is a normal uploadedfile that we can stream.
20             fp = open(full_filename, 'wb')
21             locks.lock(fp, locks.LOCK_EX)
22-            for chunk in raw_field.chunk():
23+            for chunk in raw_field.chunks():
24                 fp.write(chunk)
25             locks.unlock(fp)
26             fp.close()
27Index: django/db/models/fields/__init__.py
28===================================================================
29--- django/db/models/fields/__init__.py (revision 7827)
30+++ django/db/models/fields/__init__.py (working copy)
31@@ -766,9 +766,12 @@
32     def get_db_prep_save(self, value):
33         "Returns field's value prepared for saving into a database."
34         # Need to convert UploadedFile objects provided via a form to unicode for database insertion
35-        if value is None:
36+        if hasattr(value, 'name'):
37+            return value.name
38+        elif value is None:
39             return None
40-        return unicode(value)
41+        else:
42+            return unicode(value)
43 
44     def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
45         field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
46@@ -842,7 +845,7 @@
47             # We don't need to raise a warning because Model._save_FIELD_file will
48             # do so for us.
49             try:
50-                file_name = file.file_name
51+                file_name = file.name
52             except AttributeError:
53                 file_name = file['filename']
54 
55@@ -857,9 +860,9 @@
56         return os.path.normpath(f)
57 
58     def save_form_data(self, instance, data):
59-        from django.newforms.fields import UploadedFile
60+        from django.core.files.uploadedfile import UploadedFile
61         if data and isinstance(data, UploadedFile):
62-            getattr(instance, "save_%s_file" % self.name)(data.filename, data.data, save=False)
63+            getattr(instance, "save_%s_file" % self.name)(data.name, data, save=False)
64 
65     def formfield(self, **kwargs):
66         defaults = {'form_class': forms.FileField}
67Index: django/core/files/uploadedfile.py
68===================================================================
69--- django/core/files/uploadedfile.py   (revision 7827)
70+++ django/core/files/uploadedfile.py   (working copy)
71@@ -3,13 +3,18 @@
72 """
73 
74 import os
75+import tempfile
76+import warnings
77 try:
78     from cStringIO import StringIO
79 except ImportError:
80     from StringIO import StringIO
81 
82-__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile')
83+import settings
84 
85+
86+__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile', 'SimpleUploadedFile')
87+
88 class UploadedFile(object):
89     """
90     A abstract uploadded file (``TemporaryUploadedFile`` and
91@@ -20,34 +25,87 @@
92     """
93     DEFAULT_CHUNK_SIZE = 64 * 2**10
94 
95-    def __init__(self, file_name=None, content_type=None, file_size=None, charset=None):
96-        self.file_name = file_name
97-        self.file_size = file_size
98+    def __init__(self, name=None, content_type=None, size=None, charset=None):
99+        self.name = name
100+        self.size = size
101         self.content_type = content_type
102         self.charset = charset
103 
104     def __repr__(self):
105-        return "<%s: %s (%s)>" % (self.__class__.__name__, self.file_name, self.content_type)
106+        return "<%s: %s (%s)>" % (self.__class__.__name__, self.name, self.content_type)
107 
108-    def _set_file_name(self, name):
109+    def _get_name(self):
110+        return self._name
111+
112+    def _set_name(self, name):
113         # Sanitize the file name so that it can't be dangerous.
114         if name is not None:
115             # Just use the basename of the file -- anything else is dangerous.
116             name = os.path.basename(name)
117-           
118+
119             # File names longer than 255 characters can cause problems on older OSes.
120             if len(name) > 255:
121                 name, ext = os.path.splitext(name)
122                 name = name[:255 - len(ext)] + ext
123-               
124-        self._file_name = name
125-       
126+
127+        self._name = name
128+
129+    name = property(_get_name, _set_name)
130+
131+
132     def _get_file_name(self):
133-        return self._file_name
134-       
135-    file_name = property(_get_file_name, _set_file_name)
136+        warnings.warn(
137+            message = "To access the file name, please use the .name attribute.",
138+            category = DeprecationWarning,
139+            stacklevel = 2
140+            )
141+        return self.name
142 
143-    def chunk(self, chunk_size=None):
144+    def _set_file_name(self, name):
145+        warnings.warn(
146+            message = "To access the file name, please use the .name attribute.",
147+            category = DeprecationWarning,
148+            stacklevel = 2
149+            )
150+        self.name = name
151+
152+    file_name = filename = property(_get_file_name, _set_file_name)
153+
154+
155+    def _get_file_size(self):
156+        warnings.warn(
157+            message = "To access the file size, please use the .size attribute.",
158+            category = DeprecationWarning,
159+            stacklevel = 2
160+            )
161+        return self.size
162+
163+    def _set_file_size(self, size):
164+        warnings.warn(
165+            message = "To access the file size, please use the .size attribute.",
166+            category = DeprecationWarning,
167+            stacklevel = 2
168+            )
169+        self.size = size
170+
171+    file_size = property(_get_file_size, _set_file_size)
172+
173+
174+    def _get_data(self):
175+        warnings.warn(
176+            message = "To access the data, please use the new UploadedFile API.",
177+            category = DeprecationWarning,
178+            stacklevel = 2
179+            )
180+        data = self.read()
181+        if hasattr(self, 'seek'):
182+            self.seek(0)
183+        return data
184+
185+    data = property(_get_data)
186+
187+
188+    def chunks(self, chunk_size=None):
189         """
190         Read the file and yield chucks of ``chunk_size`` bytes (defaults to
191         ``UploadedFile.DEFAULT_CHUNK_SIZE``).
192@@ -58,12 +116,21 @@
193         if hasattr(self, 'seek'):
194             self.seek(0)
195         # Assume the pointer is at zero...
196-        counter = self.file_size
197+        counter = self.size
198 
199         while counter > 0:
200             yield self.read(chunk_size)
201             counter -= chunk_size
202 
203+    def chunk(self, *args, **kwargs):
204+        warnings.warn(
205+            message = "Please use .chunks() instead of .chunk().",
206+            category = DeprecationWarning,
207+            stacklevel = 2
208+            )
209+        for chunk in self.chunks():
210+            yield chunk
211+
212     def multiple_chunks(self, chunk_size=None):
213         """
214         Returns ``True`` if you can expect multiple chunks.
215@@ -74,9 +141,9 @@
216         """
217         if not chunk_size:
218             chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE
219-        return self.file_size < chunk_size
220+        return self.size > chunk_size
221 
222-    # Abstract methods; subclasses *must* default read() and probably should
223+    # Abstract methods; subclasses *must* define read() and probably should
224     # define open/close.
225     def read(self, num_bytes=None):
226         raise NotImplementedError()
227@@ -87,60 +154,90 @@
228     def close(self):
229         pass
230 
231+    def xreadlines(self):
232+        return self
233+
234+    def readlines(self):
235+        return list(self.xreadlines())
236+
237+    def __iter__(self):
238+        # Iterate over this file-like object by newlines
239+        buffer_ = None
240+        for chunk in self.chunks():
241+            chunk_buffer = StringIO(chunk)
242+
243+            for line in chunk_buffer:
244+                if buffer_:
245+                    line = buffer_ + line
246+                    buffer_ = None
247+
248+                # If this is the end of a line, yield
249+                # otherwise, wait for the next round
250+                if line[-1] in ('\n', '\r'):
251+                    yield line
252+                else:
253+                    buffer_ = line
254+
255+        if buffer_ is not None:
256+            yield buffer_
257+
258     # Backwards-compatible support for uploaded-files-as-dictionaries.
259     def __getitem__(self, key):
260-        import warnings
261         warnings.warn(
262             message = "The dictionary access of uploaded file objects is deprecated. Use the new object interface instead.",
263             category = DeprecationWarning,
264             stacklevel = 2
265         )
266         backwards_translate = {
267-            'filename': 'file_name',
268+            'filename': 'name',
269             'content-type': 'content_type',
270             }
271 
272         if key == 'content':
273             return self.read()
274         elif key == 'filename':
275-            return self.file_name
276+            return self.name
277         elif key == 'content-type':
278             return self.content_type
279         else:
280             return getattr(self, key)
281 
282-class TemporaryUploadedFile(UploadedFile):
283+class TemporaryUploadedFileBase(type):
284     """
285+    The temporary uploaded file object does in fact inherit from the
286+    same class that the object returned from tempfile.NamedTemporaryFile
287+    is instantiated from.
288+    """
289+    def __call__(self, name, content_type, size, charset):
290+        if settings.FILE_UPLOAD_TEMP_DIR:
291+            obj = tempfile.NamedTemporaryFile(suffix='.upload', dir=settings.FILE_UPLOAD_TEMP_DIR)
292+        else:
293+            obj = tempfile.NamedTemporaryFile(suffix='.upload')
294+
295+        obj.__class__ = type('TemporaryUploadedFile', (obj.__class__, UploadedFile), obj.__dict__)
296+
297+        UploadedFile.__init__(obj, name, content_type, size, charset)
298+        return obj
299+
300+class TemporaryUploadedFile(object):
301+    """
302     A file uploaded to a temporary location (i.e. stream-to-disk).
303     """
304+    __metaclass__ = TemporaryUploadedFileBase
305 
306-    def __init__(self, file, file_name, content_type, file_size, charset):
307-        super(TemporaryUploadedFile, self).__init__(file_name, content_type, file_size, charset)
308-        self.file = file
309-        self.path = file.name
310-        self.file.seek(0)
311-
312     def temporary_file_path(self):
313         """
314         Returns the full path of this file.
315         """
316-        return self.path
317+        return self.name
318 
319-    def read(self, *args, **kwargs):
320-        return self.file.read(*args, **kwargs)
321 
322-    def open(self):
323-        self.seek(0)
324-
325-    def seek(self, *args, **kwargs):
326-        self.file.seek(*args, **kwargs)
327-
328 class InMemoryUploadedFile(UploadedFile):
329     """
330     A file uploaded into memory (i.e. stream-to-memory).
331     """
332-    def __init__(self, file, field_name, file_name, content_type, file_size, charset):
333-        super(InMemoryUploadedFile, self).__init__(file_name, content_type, file_size, charset)
334+    def __init__(self, file, field_name, name, content_type, size, charset):
335+        super(InMemoryUploadedFile, self).__init__(name, content_type, size, charset)
336         self.file = file
337         self.field_name = field_name
338         self.file.seek(0)
339@@ -154,7 +251,7 @@
340     def read(self, *args, **kwargs):
341         return self.file.read(*args, **kwargs)
342 
343-    def chunk(self, chunk_size=None):
344+    def chunks(self, chunk_size=None):
345         self.file.seek(0)
346         yield self.read()
347 
348@@ -168,9 +265,9 @@
349     """
350     def __init__(self, name, content, content_type='text/plain'):
351         self.file = StringIO(content or '')
352-        self.file_name = name
353+        self.name = name
354         self.field_name = None
355-        self.file_size = len(content or '')
356+        self.size = len(content or '')
357         self.content_type = content_type
358         self.charset = None
359         self.file.seek(0)
360Index: django/core/files/uploadhandler.py
361===================================================================
362--- django/core/files/uploadhandler.py  (revision 7827)
363+++ django/core/files/uploadhandler.py  (working copy)
364@@ -132,21 +132,15 @@
365         Create the file object to append to as data is coming in.
366         """
367         super(TemporaryFileUploadHandler, self).new_file(file_name, *args, **kwargs)
368-        self.file = TemporaryFile(settings.FILE_UPLOAD_TEMP_DIR)
369-        self.write = self.file.write
370+        self.file = TemporaryUploadedFile(self.file_name, self.content_type, 0, self.charset)
371 
372     def receive_data_chunk(self, raw_data, start):
373-        self.write(raw_data)
374+        self.file.write(raw_data)
375 
376     def file_complete(self, file_size):
377         self.file.seek(0)
378-        return TemporaryUploadedFile(
379-            file = self.file,
380-            file_name = self.file_name,
381-            content_type = self.content_type,
382-            file_size = file_size,
383-            charset = self.charset
384-        )
385+        self.file.size = file_size
386+        return self.file
387 
388 class MemoryFileUploadHandler(FileUploadHandler):
389     """
390@@ -195,32 +189,7 @@
391             charset = self.charset
392         )
393 
394-class TemporaryFile(object):
395-    """
396-    A temporary file that tries to delete itself when garbage collected.
397-    """
398-    def __init__(self, dir):
399-        if not dir:
400-            dir = tempfile.gettempdir()
401-        try:
402-            (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir)
403-            self.file = os.fdopen(fd, 'w+b')
404-        except (OSError, IOError):
405-            raise OSError("Could not create temporary file for uploading, have you set settings.FILE_UPLOAD_TEMP_DIR correctly?")
406-        self.name = name
407 
408-    def __getattr__(self, name):
409-        a = getattr(self.__dict__['file'], name)
410-        if type(a) != type(0):
411-            setattr(self, name, a)
412-        return a
413-
414-    def __del__(self):
415-        try:
416-            os.unlink(self.name)
417-        except OSError:
418-            pass
419-
420 def load_handler(path, *args, **kwargs):
421     """
422     Given a path to a handler, return an instance of that handler.
423Index: django/newforms/fields.py
424===================================================================
425--- django/newforms/fields.py   (revision 7827)
426+++ django/newforms/fields.py   (working copy)
427@@ -27,8 +27,8 @@
428 
429 from util import ErrorList, ValidationError
430 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput
431+from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile
432 
433-
434 __all__ = (
435     'Field', 'CharField', 'IntegerField',
436     'DEFAULT_DATE_INPUT_FORMATS', 'DateField',
437@@ -419,19 +419,7 @@
438     # It's OK if Django settings aren't configured.
439     URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
440 
441-class UploadedFile(StrAndUnicode):
442-    "A wrapper for files uploaded in a FileField"
443-    def __init__(self, filename, data):
444-        self.filename = filename
445-        self.data = data
446 
447-    def __unicode__(self):
448-        """
449-        The unicode representation is the filename, so that the pre-database-insertion
450-        logic can use UploadedFile objects
451-        """
452-        return self.filename
453-
454 class FileField(Field):
455     widget = FileInput
456     default_error_messages = {
457@@ -460,23 +448,20 @@
458                 category = DeprecationWarning,
459                 stacklevel = 2
460             )
461+            data = UploadedFile(data['filename'], data['content'])
462 
463         try:
464-            file_name = data.file_name
465-            file_size = data.file_size
466+            file_name = data.name
467+            file_size = data.size
468         except AttributeError:
469-            try:
470-                file_name = data.get('filename')
471-                file_size = bool(data['content'])
472-            except (AttributeError, KeyError):
473-                raise ValidationError(self.error_messages['invalid'])
474+            raise ValidationError(self.error_messages['invalid'])
475 
476         if not file_name:
477             raise ValidationError(self.error_messages['invalid'])
478         if not file_size:
479             raise ValidationError(self.error_messages['empty'])
480 
481-        return UploadedFile(file_name, data)
482+        return data
483 
484 class ImageField(FileField):
485     default_error_messages = {
486Index: tests/modeltests/model_forms/models.py
487===================================================================
488--- tests/modeltests/model_forms/models.py      (revision 7827)
489+++ tests/modeltests/model_forms/models.py      (working copy)
490@@ -803,7 +803,7 @@
491 >>> f.is_valid()
492 True
493 >>> type(f.cleaned_data['file'])
494-<class 'django.newforms.fields.UploadedFile'>
495+<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
496 >>> instance = f.save()
497 >>> instance.file
498 u'...test1.txt'
499@@ -814,7 +814,7 @@
500 >>> f.is_valid()
501 True
502 >>> type(f.cleaned_data['file'])
503-<class 'django.newforms.fields.UploadedFile'>
504+<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
505 >>> instance = f.save()
506 >>> instance.file
507 u'...test1.txt'
508@@ -906,7 +906,7 @@
509 >>> f.is_valid()
510 True
511 >>> type(f.cleaned_data['image'])
512-<class 'django.newforms.fields.UploadedFile'>
513+<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
514 >>> instance = f.save()
515 >>> instance.image
516 u'...test.png'
517@@ -918,7 +918,7 @@
518 >>> f.is_valid()
519 True
520 >>> type(f.cleaned_data['image'])
521-<class 'django.newforms.fields.UploadedFile'>
522+<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
523 >>> instance = f.save()
524 >>> instance.image
525 u'...test.png'
526Index: tests/regressiontests/forms/fields.py
527===================================================================
528--- tests/regressiontests/forms/fields.py       (revision 7827)
529+++ tests/regressiontests/forms/fields.py       (working copy)
530@@ -800,10 +800,10 @@
531 ValidationError: [u'The submitted file is empty.']
532 
533 >>> type(f.clean(SimpleUploadedFile('name', 'Some File Content')))
534-<class 'django.newforms.fields.UploadedFile'>
535+<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
536 
537 >>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf'))
538-<class 'django.newforms.fields.UploadedFile'>
539+<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
540 
541 # URLField ##################################################################
542 
543Index: docs/upload_handling.txt
544===================================================================
545--- docs/upload_handling.txt    (revision 7827)
546+++ docs/upload_handling.txt    (working copy)
547@@ -22,7 +22,7 @@
548     class UploadFileForm(forms.Form):
549         title = forms.CharField(max_length=50)
550         file  = forms.FileField()
551-       
552+
553 A view handling this form will receive the file data in ``request.FILES``, which
554 is a dictionary containing a key for each ``FileField`` (or ``ImageField``, or
555 other ``FileField`` subclass) in the form. So the data from the above form would
556@@ -64,32 +64,32 @@
557     ``UploadedFile.read()``
558         Read the entire uploaded data from the file. Be careful with this
559         method: if the uploaded file is huge it can overwhelm your system if you
560-        try to read it into memory. You'll probably want to use ``chunk()``
561+        try to read it into memory. You'll probably want to use ``chunks()``
562         instead; see below.
563-       
564+
565     ``UploadedFile.multiple_chunks()``
566         Returns ``True`` if the uploaded file is big enough to require
567         reading in multiple chunks. By default this will be any file
568         larger than 2.5 megabytes, but that's configurable; see below.
569-   
570+
571     ``UploadedFile.chunk()``
572         A generator returning chunks of the file. If ``multiple_chunks()`` is
573         ``True``, you should use this method in a loop instead of ``read()``.
574-       
575+
576         In practice, it's often easiest simply to use ``chunks()`` all the time;
577         see the example below.
578-   
579+
580     ``UploadedFile.file_name``
581         The name of the uploaded file (e.g. ``my_file.txt``).
582-       
583+
584     ``UploadedFile.file_size``
585         The size, in bytes, of the uploaded file.
586-       
587+
588 There are a few other methods and attributes available on ``UploadedFile``
589 objects; see `UploadedFile objects`_ for a complete reference.
590 
591 Putting it all together, here's a common way you might handle an uploaded file::
592-   
593+
594     def handle_uploaded_file(f):
595         destination = open('some/file/name.txt', 'wb')
596         for chunk in f.chunks():
597@@ -126,27 +126,27 @@
598         The maximum size, in bytes, for files that will be uploaded
599         into memory. Files larger than ``FILE_UPLOAD_MAX_MEMORY_SIZE``
600         will be streamed to disk.
601-       
602+
603         Defaults to 2.5 megabytes.
604-       
605+
606     ``FILE_UPLOAD_TEMP_DIR``
607         The directory where uploaded files larger than ``FILE_UPLOAD_TEMP_DIR``
608         will be stored.
609-       
610+
611         Defaults to your system's standard temporary directory (i.e. ``/tmp`` on
612         most Unix-like systems).
613-       
614+
615     ``FILE_UPLOAD_HANDLERS``
616         The actual handlers for uploaded files. Changing this setting
617         allows complete customization -- even replacement -- of
618         Django's upload process. See `upload handlers`_, below,
619         for details.
620-       
621+
622         Defaults to::
623-       
624+
625             ("django.core.files.uploadhandler.MemoryFileUploadHandler",
626              "django.core.files.uploadhandler.TemporaryFileUploadHandler",)
627-           
628+
629         Which means "try to upload to memory first, then fall back to temporary
630         files."
631 
632@@ -161,35 +161,39 @@
633         Returns a byte string of length ``num_bytes``, or the complete file if
634         ``num_bytes`` is ``None``.
635 
636-    ``UploadedFile.chunk(self, chunk_size=None)``
637+    ``UploadedFile.chunks(self, chunk_size=None)``
638         A generator yielding small chunks from the file. If ``chunk_size`` isn't
639-        given, chunks will be 64 kb.
640+        given, chunks will be 64 KB.
641 
642     ``UploadedFile.multiple_chunks(self, chunk_size=None)``
643         Returns ``True`` if you can expect more than one chunk when calling
644-        ``UploadedFile.chunk(self, chunk_size)``.
645+        ``UploadedFile.chunks(self, chunk_size)``.
646 
647     ``UploadedFile.file_size``
648         The size, in bytes, of the uploaded file.
649-   
650+
651     ``UploadedFile.file_name``
652         The name of the uploaded file as provided by the user.
653-   
654+
655     ``UploadedFile.content_type``
656         The content-type header uploaded with the file (e.g. ``text/plain`` or
657         ``application/pdf``). Like any data supplied by the user, you shouldn't
658         trust that the uploaded file is actually this type. You'll still need to
659         validate that the file contains the content that the content-type header
660         claims -- "trust but verify."
661-   
662+
663     ``UploadedFile.charset``
664         For ``text/*`` content-types, the character set (i.e. ``utf8``) supplied
665         by the browser. Again, "trust but verify" is the best policy here.
666 
667+    ``UploadedFile.__iter__()``
668+        Iterates over the lines in the file.
669+
670     ``UploadedFile.temporary_file_path()``
671         Only files uploaded onto disk will have this method; it returns the full
672         path to the temporary uploaded file.
673 
674+
675 Upload Handlers
676 ===============
677 
678Index: docs/newforms.txt
679===================================================================
680--- docs/newforms.txt   (revision 7827)
681+++ docs/newforms.txt   (working copy)
682@@ -1331,23 +1331,12 @@
683     * Validates that non-empty file data has been bound to the form.
684     * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``
685 
686-An ``UploadedFile`` object has two attributes:
687+To learn more about the ``UploadedFile`` object, see the `file uploads documentation`_.
688 
689-    ======================  ====================================================
690-    Attribute               Description
691-    ======================  ====================================================
692-    ``filename``            The name of the file, provided by the uploading
693-                            client.
694-                           
695-    ``content``             The array of bytes comprising the file content.
696-    ======================  ====================================================
697-
698-The string representation of an ``UploadedFile`` is the same as the filename
699-attribute.
700-
701 When you use a ``FileField`` on a form, you must also remember to
702 `bind the file data to the form`_.
703 
704+.. _file uploads documentation: ../upload_handling/
705 .. _`bind the file data to the form`: `Binding uploaded files to a form`_
706 
707 ``FilePathField``
708Index: docs/settings.txt
709===================================================================
710--- docs/settings.txt   (revision 7827)
711+++ docs/settings.txt   (working copy)
712@@ -279,7 +279,7 @@
713 
714 The database backend to use. The build-in database backends are
715 ``'postgresql_psycopg2'``, ``'postgresql'``, ``'mysql'``, ``'mysql_old'``,
716-``'sqlite3'``, ``'oracle'``, and ``'oracle'``.
717+``'sqlite3'``, and ``'oracle'``.
718 
719 In the Django development version, you can use a database backend that doesn't
720 ship with Django by setting ``DATABASE_ENGINE`` to a fully-qualified path (i.e.