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
|
- /trunk/reviewboard/manage.py: 2 changes [ 1 2 ]
- /trunk/reviewboard/test.py: 3 changes [ 1 2 3 ]
- /trunk/reviewboard/diffviewer/forms.py: 6 changes [ 1 2 3 4 5 6 ]
- /trunk/reviewboard/reviews/forms.py: 14 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ]
- /trunk/reviewboard/reviews/models.py: 2 changes [ 1 2 ]
- /trunk/reviewboard/reviews/views.py: 2 changes [ 1 2 ]
- /trunk/reviewboard/webapi/json.py: 3 changes [ 1 2 3 ]
- /trunk/reviewboard/webapi/tests.py: 4 changes [ 1 2 3 4 ]
| /trunk/reviewboard/manage.py | |||
|---|---|---|---|
| Revision 1389 | New Change | ||
| ... | 30 lines hidden [Expand] | ||
| 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: |
| ... | 87 lines hidden [Expand] | ||
| /trunk/reviewboard/test.py | |||
|---|---|---|---|
| Revision 1389 | New Change | ||
| ... | 20 lines hidden [Expand] | ||
| 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 | ||
| ... | 9 lines hidden [Expand] | ||
| 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) |
| ... | 16 lines hidden [Expand] | ||
| 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 | ||
| ... | 17 lines hidden [Expand] | ||
| 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++"] |
| ... | 6 lines hidden [Expand] | ||
| 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: |
| ... | 10 lines hidden [Expand] | ||
| 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 | ||
| ... | 21 lines hidden [Expand] | ||
| 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("\\", "/") |
| ... | 31 lines hidden [Expand] | ||
| /trunk/reviewboard/reviews/forms.py | |||
|---|---|---|---|
| Revision 1389 | New Change | ||
| 1 |
|
1 |
|
| 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 |
| ... | 14 lines hidden [Expand] | ||
| 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) |
| ... | 5 lines hidden [Expand] | ||
| 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. |
| ... | 33 lines hidden [Expand] | ||
| 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() |
| ... | 17 lines hidden [Expand] | ||
| 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 | ||
| ... | 118 lines hidden [Expand] | ||
| 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. |
| ... | 796 lines hidden [Expand] | ||
| /trunk/reviewboard/reviews/views.py | |||
|---|---|---|---|
| Revision 1389 | New Change | ||
| ... | 49 lines hidden [Expand] | ||
| 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 |
| ... | 551 lines hidden [Expand] | ||
| /trunk/reviewboard/webapi/json.py | |||
|---|---|---|---|
| Revision 1389 | New Change | ||
| ... | 1008 lines hidden [Expand] | ||
| 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 |
| ... | 64 lines hidden [Expand] | ||
| 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 | }, |
| ... | 160 lines hidden [Expand] | ||
| /trunk/reviewboard/webapi/tests.py | |||
|---|---|---|---|
| Revision 1389 | New Change | ||
| ... | 38 lines hidden [Expand] | ||
| 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. |