Review Board

beta

Improve file uploading on newer Djangos.

Updated 5 months, 1 week ago

Christian Hammond Reviewers
trunk reviewboard
None Review Board SVN
Migrate file uploading support to use the newer ImageField and FileField, as well as the new uploading classes in Django. This ends up cleaning up our code a little bit.

We also now upload images to /year/month/day directories.

This also changes unit tests to write files to a temporary directory so we don't litter the actual upload directory.
Verified that I could still upload diffs and images without problems. Also verified that the internal validation of correct image types works.

This passed all unit tests (well, as well as it did before -- there's some issue with writing 0-byte images to disk, and I think it's a Django bug. Still investigating.)

Diff revision 2 (Latest)

1 2
1 2

  1. /trunk/reviewboard/manage.py: 2 changes [ 1 2 ]
  2. /trunk/reviewboard/test.py: 3 changes [ 1 2 3 ]
  3. /trunk/reviewboard/diffviewer/forms.py: 6 changes [ 1 2 3 4 5 6 ]
  4. /trunk/reviewboard/reviews/forms.py: 14 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ]
  5. /trunk/reviewboard/reviews/models.py: 2 changes [ 1 2 ]
  6. /trunk/reviewboard/reviews/views.py: 2 changes [ 1 2 ]
  7. /trunk/reviewboard/webapi/json.py: 3 changes [ 1 2 3 ]
  8. /trunk/reviewboard/webapi/tests.py: 4 changes [ 1 2 3 4 ]
/trunk/reviewboard/manage.py
Revision 1389 New Change
31
    try:
31
    try:
32
        import django
32
        import django
33
        if django.VERSION[0] == 0 and django.VERSION[1] < 97:
33
        if django.VERSION[0] == 0 and django.VERSION[1] < 97:
34
            raise ImportError
34
            raise ImportError
35
35
36
        # QuerySet refactor (r7477)
36
        # UploadedFile support (~r7829)
37
        from django.core.exceptions import FieldError
37
        from django.core.files.uploadedfile import UploadedFile
38
    except ImportError:
38
    except ImportError:
39
        dependency_error("Django > 0.97 (or SVN >= r7477) is required.")
39
        dependency_error("Django > 0.97 (or SVN >= r7829) is required.")
40
40
41
    # django-evolution
41
    # django-evolution
42
    try:
42
    try:
43
        import django_evolution
43
        import django_evolution
44
    except ImportError:
44
    except ImportError:
/trunk/reviewboard/test.py
Revision 1389 New Change
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
24
#
25
25
26
import os
27
import os.path
26
import sys
28
import sys
27
29
28
import djblets
30
import djblets
29
import nose
31
import nose
30
32
40
    setup_test_environment()
42
    setup_test_environment()
41
    settings.DEBUG = False
43
    settings.DEBUG = False
42
44
43
    # Default to testing in a non-subdir install.
45
    # Default to testing in a non-subdir install.
44
    settings.SITE_ROOT = "/"
46
    settings.SITE_ROOT = "/"
47
    settings.MEDIA_ROOT = "/tmp/reviewboard-tests"
48
49
    images_dir = os.path.join(settings.MEDIA_ROOT, "uploaded", "images")
50
51
    if not os.path.exists(images_dir):
52
        os.makedirs(images_dir)
53
45
    settings.MEDIA_URL = settings.SITE_ROOT + 'media/'
54
    settings.MEDIA_URL = settings.SITE_ROOT + 'media/'
46
    settings.ADMIN_MEDIA_PREFIX = settings.MEDIA_URL + 'admin/'
55
    settings.ADMIN_MEDIA_PREFIX = settings.MEDIA_URL + 'admin/'
47
56
48
    old_name = settings.DATABASE_NAME
57
    old_name = settings.DATABASE_NAME
49
    create_test_db(verbosity, autoclobber=not interactive)
58
    create_test_db(verbosity, autoclobber=not interactive)
66
75
67
    if len(sys.argv) > 2:
76
    if len(sys.argv) > 2:
68
        nose_argv += sys.argv[2:]
77
        nose_argv += sys.argv[2:]
69
    nose.main(argv=nose_argv)
78
    nose.main(argv=nose_argv)
70
79
80
    for root, dirs, files in walk(settings.MEDIA_ROOT, topdown=False):
81
        for name in files:
82
            os.remove(os.path.join(root, name))
83
84
        for name in dirs:
85
            os.rmdir(os.path.join(root, name))
86
71
    destroy_test_db(old_name, verbosity)
87
    destroy_test_db(old_name, verbosity)
72
    teardown_test_environment()
88
    teardown_test_environment()
/trunk/reviewboard/diffviewer/forms.py
Revision 1389 New Change
18
    pass
18
    pass
19
19
20
20
21
class UploadDiffForm(forms.Form):
21
class UploadDiffForm(forms.Form):
22
    basedir = forms.CharField(label=_("Base directory"))
22
    basedir = forms.CharField(label=_("Base directory"))
23
    path = forms.CharField(label=_("Diff"), widget=forms.FileInput())
23
    path = forms.FileField(label=_("Diff"))
24
    parent_diff_path = forms.CharField(label=_("Parent diff (optional)"),
24
    parent_diff_path = forms.FileField(label=_("Parent diff (optional)"),
25
                                       widget=forms.FileInput(),
26
                                       required=False)
25
                                       required=False)
27
26
28
    # Extensions used for intelligent sorting of header files
27
    # Extensions used for intelligent sorting of header files
29
    # before implementation files.
28
    # before implementation files.
30
    HEADER_EXTENSIONS = ["h", "H", "hh", "hpp", "hxx", "h++"]
29
    HEADER_EXTENSIONS = ["h", "H", "hh", "hpp", "hxx", "h++"]
37
        if self.repository.get_scmtool().get_diffs_use_absolute_paths():
36
        if self.repository.get_scmtool().get_diffs_use_absolute_paths():
38
            # This SCMTool uses absolute paths, so there's no need to ask
37
            # This SCMTool uses absolute paths, so there's no need to ask
39
            # the user for the base directory.
38
            # the user for the base directory.
40
            del(self.fields['basedir'])
39
            del(self.fields['basedir'])
41
40
42
    def create(self, diff_file, parent_diff_file=None, diffset_history=None):
41
    def create(self, diffset_history=None):
43
        tool = self.repository.get_scmtool()
42
        tool = self.repository.get_scmtool()
44
43
45
        # Grab the base directory if there is one.
44
        # Grab the base directory if there is one.
46
        if not tool.get_diffs_use_absolute_paths():
45
        if not tool.get_diffs_use_absolute_paths():
47
            try:
46
            try:
48
                basedir = smart_unicode(self.cleaned_data['basedir'])
47
                basedir = smart_unicode(self.cleaned_data['basedir'])
49
            except AttributeError:
48
            except AttributeError:
50
                raise NoBaseDirError(_('The "Base Diff Path" field is required'))
49
                raise NoBaseDirError(_('The "Base Diff Path" field is required'))
51
        else:
50
        else:
52
            basedir = ''
51
            basedir = ''
53
52
53
        diff_file = self.cleaned_data['path']
54
        parent_diff_file = self.cleaned_data['parent_diff_path']
55
54
        # Parse the diff
56
        # Parse the diff
55
        files = list(self._process_files(
57
        files = list(self._process_files(
56
            diff_file, basedir, check_existance=(parent_diff_file is not None)))
58
            diff_file, basedir, check_existance=(parent_diff_file is not None)))
57
59
58
        if len(files) == 0:
60
        if len(files) == 0:
69
            # later apply each of the files that are in the main diff
71
            # later apply each of the files that are in the main diff
70
            for f in self._process_files(parent_diff_file, basedir,
72
            for f in self._process_files(parent_diff_file, basedir,
71
                                         check_existance=True):
73
                                         check_existance=True):
72
                parent_files[f.origFile] = f
74
                parent_files[f.origFile] = f
73
75
74
        diffset = DiffSet(name=diff_file["filename"], revision=0,
76
        diffset = DiffSet(name=diff_file.filename, revision=0,
75
                          history=diffset_history,
77
                          history=diffset_history,
76
                          diffcompat=DEFAULT_DIFF_COMPAT_VERSION)
78
                          diffcompat=DEFAULT_DIFF_COMPAT_VERSION)
77
        diffset.repository = self.repository
79
        diffset.repository = self.repository
78
        diffset.save()
80
        diffset.save()
79
81
101
        return diffset
103
        return diffset
102
104
103
    def _process_files(self, file, basedir, check_existance=False):
105
    def _process_files(self, file, basedir, check_existance=False):
104
        tool = self.repository.get_scmtool()
106
        tool = self.repository.get_scmtool()
105
107
106
        for f in tool.get_parser(file["content"]).parse():
108
        for f in tool.get_parser(file.data.read()).parse():
107
            f2, revision = tool.parse_diff_revision(f.origFile, f.origInfo)
109
            f2, revision = tool.parse_diff_revision(f.origFile, f.origInfo)
108
            if f2.startswith("/"):
110
            if f2.startswith("/"):
109
                filename = f2
111
                filename = f2
110
            else:
112
            else:
111
                filename = os.path.join(basedir, f2).replace("\\", "/")
113
                filename = os.path.join(basedir, f2).replace("\\", "/")
/trunk/reviewboard/reviews/forms.py
Revision 1389 New Change
1
import re
1
import re
2
import os.path
2
3
3
from django import newforms as forms
4
from django import newforms as forms
4
from django.utils.translation import ugettext as _
5
from django.utils.translation import ugettext as _
5
from PIL import Image
6
6
7
from reviewboard.diffviewer.forms import UploadDiffForm, EmptyDiffError
7
from reviewboard.diffviewer.forms import UploadDiffForm, EmptyDiffError
8
from reviewboard.reviews.models import ReviewRequest, \
8
from reviewboard.reviews.models import ReviewRequest, \
9
                                       ReviewRequestDraft, Screenshot, \
9
                                       ReviewRequestDraft, Screenshot, \
10
                                       ChangeNumberInUseError
10
                                       ChangeNumberInUseError
25
    information on the diffs, the repository the diffs are against, and
25
    information on the diffs, the repository the diffs are against, and
26
    optionally a changelist number (for use in certain repository types
26
    optionally a changelist number (for use in certain repository types
27
    such as Perforce).
27
    such as Perforce).
28
    """
28
    """
29
    basedir = forms.CharField(label=_("Base Diff Path"), required=False)
29
    basedir = forms.CharField(label=_("Base Diff Path"), required=False)
30
    diff_path = forms.CharField(label=_("Diff"),
30
    diff_path = forms.FileField(label=_("Diff"), required=True)
31
                                widget=forms.FileInput, required=True)
31
    parent_diff_path = forms.FileField(label=_("Parent Diff (optional)"),
32
    parent_diff_path = forms.CharField(label=_("Parent Diff (optional)"),
32
                                       required=False)
33
                                       widget=forms.FileInput, required=False)
34
    repository = forms.ChoiceField(label=_("Repository"), required=True)
33
    repository = forms.ChoiceField(label=_("Repository"), required=True)
35
    changenum = forms.IntegerField(label=_("Change Number"), required=False)
34
    changenum = forms.IntegerField(label=_("Change Number"), required=False)
36
35
37
    def __init__(self, *args, **kwargs):
36
    def __init__(self, *args, **kwargs):
38
        forms.Form.__init__(self, *args, **kwargs)
37
        forms.Form.__init__(self, *args, **kwargs)
44
        """Helper function to combine the common bits of clean_target_people
43
        """Helper function to combine the common bits of clean_target_people
45
           and clean_target_groups"""
44
           and clean_target_groups"""
46
        names = [x for x in map(str.strip, re.split(',\s*', data)) if x]
45
        names = [x for x in map(str.strip, re.split(',\s*', data)) if x]
47
        return set([constructor(name) for name in names])
46
        return set([constructor(name) for name in names])
48
47
49
    def create(self, user, diff_file, parent_diff_file):
48
    def create(self, user):
50
        repository = Repository.objects.get(pk=self.cleaned_data['repository'])
49
        repository = Repository.objects.get(pk=self.cleaned_data['repository'])
51
        changenum = self.cleaned_data['changenum'] or None
50
        changenum = self.cleaned_data['changenum'] or None
52
51
53
        # It's a little odd to validate this here, but we want to have access to
52
        # It's a little odd to validate this here, but we want to have access to
54
        # the user.
53
        # the user.
88
87
89
        diff_form = UploadDiffForm(repository, data={
88
        diff_form = UploadDiffForm(repository, data={
90
            'basedir': self.cleaned_data['basedir'],
89
            'basedir': self.cleaned_data['basedir'],
91
        },
90
        },
92
        files={
91
        files={
93
            'path': self.cleaned_data['diff_path'],
92
            'path': self.cleaned_data['diff_path'].data,
94
            'parent_diff_path': self.cleaned_data['parent_diff_path'],
93
            'parent_diff_path': getattr(self.cleaned_data['parent_diff_path'],
94
                                        "data", None),
95
        })
95
        })
96
        diff_form.full_clean()
96
        diff_form.full_clean()
97
97
98
        try:
98
        try:
99
            diff_form.create(diff_file, parent_diff_file,
99
            diff_form.create(review_request.diffset_history)
100
                             review_request.diffset_history)
101
            if 'path' in diff_form.errors:
100
            if 'path' in diff_form.errors:
102
                review_request.delete()
101
                review_request.delete()
103
                self.errors['diff_path'] = diff_form.errors['path']
102
                self.errors['diff_path'] = diff_form.errors['path']
104
            elif 'base_diff_path' in diff_form.errors:
103
            elif 'base_diff_path' in diff_form.errors:
105
                review_request.delete()
104
                review_request.delete()
123
    """
122
    """
124
    A form that handles uploading of new screenshots.
123
    A form that handles uploading of new screenshots.
125
    A screenshot takes a path argument and optionally a caption.
124
    A screenshot takes a path argument and optionally a caption.
126
    """
125
    """
127
    caption = forms.CharField(required=False)
126
    caption = forms.CharField(required=False)
128
    path = forms.CharField(widget=forms.FileInput())
127
    path = forms.ImageField(required=True)
129
128
130
    def create(self, data, review):
129
    def create(self, review):
131
        screenshot = Screenshot(caption=self.cleaned_data['caption'],
130
        screenshot = Screenshot(caption=self.cleaned_data['caption'],
132
                                draft_caption=self.cleaned_data['caption'])
131
                                draft_caption=self.cleaned_data['caption'])
133
        screenshot.save()
134
        screenshot.save_image_file(data['filename'], data['content'])
135
132
136
        try:
133
        # This implies screenshot.save()
137
            image = Image.open(screenshot.get_image_filename())
134
        uploaded_file = self.cleaned_data['path']
138
            image.load()
135
        uploaded_file.data.open()
139
        except:
136
        screenshot.save_image_file(uploaded_file.filename, uploaded_file.data)
140
            screenshot.delete()
141
            raise ValueError('The file does not appear to be an image')
142
137
143
        draft = ReviewRequestDraft.create(review)
138
        draft = ReviewRequestDraft.create(review)
144
        review.screenshots.add(screenshot)
139
        review.screenshots.add(screenshot)
145
        draft.screenshots.add(screenshot)
140
        draft.screenshots.add(screenshot)
146
        draft.save()
141
        draft.save()
147
142
148
        return screenshot
143
        return screenshot
/trunk/reviewboard/reviews/models.py
Revision 1389 New Change
119
    """
119
    """
120
    caption = models.CharField(_("caption"), max_length=256, blank=True)
120
    caption = models.CharField(_("caption"), max_length=256, blank=True)
121
    draft_caption = models.CharField(_("draft caption"),
121
    draft_caption = models.CharField(_("draft caption"),
122
                                     max_length=256, blank=True)
122
                                     max_length=256, blank=True)
123
    image = models.ImageField(_("image"),
123
    image = models.ImageField(_("image"),
124
                              upload_to=os.path.join('uploaded', 'images'))
124
                              upload_to=os.path.join('uploaded', 'images',
125
                                                     '%Y', '%m', '%d'))
125
126
126
    def thumb(self):
127
    def thumb(self):
127
        """
128
        """
128
        Creates a thumbnail of this screenshot and returns the HTML
129
        Creates a thumbnail of this screenshot and returns the HTML
129
        output embedding the thumbnail.
130
        output embedding the thumbnail.
/trunk/reviewboard/reviews/views.py
Revision 1389 New Change
50
    if request.method == 'POST':
50
    if request.method == 'POST':
51
        form = NewReviewRequestForm(request.POST, request.FILES)
51
        form = NewReviewRequestForm(request.POST, request.FILES)
52
52
53
        if form.is_valid():
53
        if form.is_valid():
54
            try:
54
            try:
55
                review_request = form.create(
55
                review_request = form.create(request.user)
56
                    user=request.user,
57
                    diff_file=request.FILES['diff_path'],
58
                    parent_diff_file=request.FILES.get('parent_diff_path'))
59
                return HttpResponseRedirect(review_request.get_absolute_url())
56
                return HttpResponseRedirect(review_request.get_absolute_url())
60
            except:
57
            except:
61
                # XXX - OwnershipError or ChangeSetError?
58
                # XXX - OwnershipError or ChangeSetError?
62
                #
59
                #
63
                # We're preventing an exception from being thrown here so that
60
                # We're preventing an exception from being thrown here so that
/trunk/reviewboard/webapi/json.py
Revision 1389 New Change
1009
1009
1010
    if not form.is_valid():
1010
    if not form.is_valid():
1011
        return WebAPIResponseFormError(request, form)
1011
        return WebAPIResponseFormError(request, form)
1012
1012
1013
    try:
1013
    try:
1014
        diffset = form.create(request.FILES['path'],
1014
        diffset = form.create()
1015
                              request.FILES.get('parent_diff_path'))
1016
1015
1017
        # Set the initial revision to be one newer than the most recent
1016
        # Set the initial revision to be one newer than the most recent
1018
        # public revision, so we can reference it in the diff viewer.
1017
        # public revision, so we can reference it in the diff viewer.
1019
        #
1018
        #
1020
        # TODO: It would be nice to later consolidate this with the logic in
1019
        # TODO: It would be nice to later consolidate this with the logic in
1085
1084
1086
    if not form.is_valid():
1085
    if not form.is_valid():
1087
        return WebAPIResponseFormError(request, form)
1086
        return WebAPIResponseFormError(request, form)
1088
1087
1089
    try:
1088
    try:
1090
        screenshot = form.create(request.FILES['path'], review_request)
1089
        screenshot = form.create(review_request)
1091
    except ValueError, e:
1090
    except ValueError, e:
1092
        return WebAPIResponseError(request, INVALID_FORM_DATA, {
1091
        return WebAPIResponseError(request, INVALID_FORM_DATA, {
1093
            'fields': {
1092
            'fields': {
1094
                'path': [str(e)],
1093
                'path': [str(e)],
1095
            },
1094
            },
/trunk/reviewboard/webapi/tests.py
Revision 1389 New Change
39
        print "Response: %s" % rsp
39
        print "Response: %s" % rsp
40
        return rsp
40
        return rsp
41
41
42
    def apiPost(self, path, query={}):
42
    def apiPost(self, path, query={}):
43
        print "Posting to /api/json/%s/" % path
43
        print "Posting to /api/json/%s/" % path
44
        print "Post data: %s" % query
44
        response = self.client.post("/api/json/%s/" % path, query)
45
        response = self.client.post("/api/json/%s/" % path, query)
45
        self.assertEqual(response.status_code, 200)
46
        self.assertEqual(response.status_code, 200)
46
        rsp = simplejson.loads(response.content)
47
        rsp = simplejson.loads(response.