Repository: pypi/warehouse

Issue:
Admin package upload size text control should use MB instead of bytes
Context: https://github.com/pypa/warehouse/pull/2470#issuecomment-334852617

Decision:

> The input in should be in MB and should just be converted to/from bytes in the backend.

Also:

> It also might be a good idea to flash an error and redirect back to the detail page if you try to set a limit less then the current default minimum.

Additionally:

> [The form field] would be a bit nicer as <input type="number" min={{ THE MINMUM SIZE }} ...> instead of as a text field. It might also make sense to include step="10" or something


Discussion:
@dstufft I'm a PyPA member but not a member on this project so I can't assign myself.

Resolving patch:
diff --git a/warehouse/admin/views/projects.py b/warehouse/admin/views/projects.py
--- a/warehouse/admin/views/projects.py
+++ b/warehouse/admin/views/projects.py
@@ -24,6 +24,9 @@
 from warehouse.accounts.models import User
 from warehouse.packaging.models import Project, Release, Role, JournalEntry
 from warehouse.utils.paginate import paginate_url_factory
+from warehouse.forklift.legacy import MAX_FILESIZE
+
+ONE_MB = 1024 * 1024  # bytes
 
 
 @view_config(
@@ -101,7 +104,13 @@ def project_detail(project, request):
         )
     ]
 
-    return {"project": project, "maintainers": maintainers, "journal": journal}
+    return {
+        "project": project,
+        "maintainers": maintainers,
+        "journal": journal,
+        "ONE_MB": ONE_MB,
+        "MAX_FILESIZE": MAX_FILESIZE
+    }
 
 
 @view_config(
@@ -218,18 +227,27 @@ def set_upload_limit(project, request):
     # If the upload limit is an empty string or othrwise falsy, just set the
     # limit to None, indicating the default limit.
     if not upload_limit:
-        project.upload_limit = None
+        upload_limit = None
     else:
         try:
-            project.upload_limit = int(upload_limit)
+            upload_limit = int(upload_limit)
         except ValueError:
             raise HTTPBadRequest(
-                f"Invalid value for upload_limit: {upload_limit}, "
+                f"Invalid value for upload limit: {upload_limit}, "
                 f"must be integer or empty string.")
 
+        # The form is in MB, but the database field is in bytes.
+        upload_limit *= ONE_MB
+
+        if upload_limit < MAX_FILESIZE:
+            raise HTTPBadRequest(
+                f"Upload limit can not be less than the default limit of "
+                f"{MAX_FILESIZE / ONE_MB}MB.")
+
+    project.upload_limit = upload_limit
+
     request.session.flash(
-        f"Successfully set the upload limit on {project.name!r} to "
-        f"{project.upload_limit!r}",
+        f"Successfully set the upload limit on {project.name!r}.",
         queue="success",
     )
 


Test patch:
diff --git a/tests/unit/admin/views/test_projects.py b/tests/unit/admin/views/test_projects.py
--- a/tests/unit/admin/views/test_projects.py
+++ b/tests/unit/admin/views/test_projects.py
@@ -354,21 +354,21 @@ def test_sets_limitwith_integer(self, db_request):
             flash=pretend.call_recorder(lambda *a, **kw: None),
         )
         db_request.matchdict["project_name"] = project.normalized_name
-        db_request.POST["upload_limit"] = "12345"
+        db_request.POST["upload_limit"] = "90"
 
         views.set_upload_limit(project, db_request)
 
         assert db_request.session.flash.calls == [
             pretend.call(
-                "Successfully set the upload limit on 'foo' to 12345",
+                "Successfully set the upload limit on 'foo'.",
                 queue="success"),
         ]
 
-        assert project.upload_limit == 12345
+        assert project.upload_limit == 90 * views.ONE_MB
 
     def test_sets_limit_with_none(self, db_request):
         project = ProjectFactory.create(name="foo")
-        project.upload_limit = 12345
+        project.upload_limit = 90 * views.ONE_MB
 
         db_request.route_path = pretend.call_recorder(
             lambda *a, **kw: "/admin/projects/")
@@ -381,13 +381,13 @@ def test_sets_limit_with_none(self, db_request):
 
         assert db_request.session.flash.calls == [
             pretend.call(
-                "Successfully set the upload limit on 'foo' to None",
+                "Successfully set the upload limit on 'foo'.",
                 queue="success"),
         ]
 
         assert project.upload_limit is None
 
-    def test_sets_limit_with_bad_value(self, db_request):
+    def test_sets_limit_with_non_integer(self, db_request):
         project = ProjectFactory.create(name="foo")
 
         db_request.matchdict["project_name"] = project.normalized_name
@@ -395,3 +395,12 @@ def test_sets_limit_with_bad_value(self, db_request):
 
         with pytest.raises(HTTPBadRequest):
             views.set_upload_limit(project, db_request)
+
+    def test_sets_limit_with_less_than_minimum(self, db_request):
+        project = ProjectFactory.create(name="foo")
+
+        db_request.matchdict["project_name"] = project.normalized_name
+        db_request.POST["upload_limit"] = "20"
+
+        with pytest.raises(HTTPBadRequest):
+            views.set_upload_limit(project, db_request)