Skip to content

Commit b6c2898

Browse files
authored
Merge pull request #53 from octue/unfold-support
Unfold support
2 parents 9883a6e + 1b693b5 commit b6c2898

File tree

11 files changed

+162
-26
lines changed

11 files changed

+162
-26
lines changed

django_gcp/static/django_gcp/cloud_object_widget.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
margin-left: 0; */
7575
}
7676

77-
.gcp-clear-selected {
77+
.gcp-button-alert {
7878
display: inline-block;
7979
text-decoration: none;
8080
color: rgb(229, 185, 26);

django_gcp/static/django_gcp/cloud_object_widget.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function clearSelected(wrapper) {
7373
wrapper.find(".gcp-selected-name").text("");
7474
// Update button text to "Select file"
7575
wrapper.find(".gcp-select-label").text("Select file");
76-
wrapper.find(".gcp-clear-selected").addClass("gcp-disabled").prop("disabled", true);
76+
wrapper.find(".gcp-clear-selected").addClass("gcp-disabled hidden").prop("disabled", true);
7777
}
7878

7979
function addSelected(wrapper, name, contentType) {
@@ -93,7 +93,7 @@ function addSelected(wrapper, name, contentType) {
9393
// Update button text to "Select other"
9494
wrapper.find(".gcp-select-label").text("Select other");
9595
// Enable user to clear selection
96-
wrapper.find(".gcp-clear-selected").removeClass("gcp-disabled").prop("disabled", false);
96+
wrapper.find(".gcp-clear-selected").removeClass("gcp-disabled hidden").prop("disabled", false);
9797
}
9898

9999
function hasSelected(wrapper) {

django_gcp/storage/fields.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from uuid import uuid4
55
from django.conf import settings
6+
from django.contrib.admin.widgets import AdminTextareaWidget
67
from django.core import checks
78
from django.core.exceptions import ValidationError
89
from django.db import models, transaction
@@ -164,7 +165,7 @@ def deconstruct(self):
164165

165166
def formfield(self, **kwargs):
166167
widget = kwargs.pop("widget", None)
167-
if widget is None:
168+
if widget is None or issubclass(widget, AdminTextareaWidget):
168169
widget = CloudObjectWidget(
169170
accept_mimetype=self.accept_mimetype,
170171
signed_ingress_url=self._get_signed_ingress_url(),
@@ -207,7 +208,6 @@ def pre_save(self, model_instance, add):
207208
)
208209
new_value = value
209210
else:
210-
211211
# There are six scenarios to deal with:
212212
adding_blank = add and value is None
213213
adding_valid = add and value is not None
@@ -232,7 +232,7 @@ def on_commit_blank():
232232

233233
elif adding_valid or updating_blank_to_valid or updating_valid_to_valid:
234234
new_value = {}
235-
new_value["path"], allow_overwrite = self.get_destination_path(
235+
new_value["path"], allow_overwrite = self._get_destination_path(
236236
instance=model_instance,
237237
original_name=value["name"],
238238
attributes=getattr(value, "attributes", None),
@@ -548,3 +548,15 @@ def _get_valid_to_valid(self, instance):
548548
existing_path = self._get_existing_path(instance)
549549
temporary_path = self._get_instance_tmp_path(instance)
550550
return existing_path is not None and temporary_path is not None
551+
552+
def _get_destination_path(self, *args, **kwargs):
553+
"""Call the get_destination_path callback unless an override is defined in
554+
settings. This funcitonality is intended for test purposes only, because
555+
patching the callback in a test framework is a struggle
556+
"""
557+
get_destination_path = getattr(
558+
settings,
559+
"GCP_STORAGE_OVERRIDE_GET_DESTINATION_PATH_CALLBACK",
560+
self.get_destination_path,
561+
)
562+
return get_destination_path(*args, **kwargs)

django_gcp/storage/widgets.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from django.conf import settings
23
from django.forms import Widget
34

45

@@ -38,6 +39,9 @@ def __init__(
3839
self.signed_ingress_url = signed_ingress_url
3940
self.ingress_path = ingress_path
4041

42+
if "unfold" in settings.INSTALLED_APPS:
43+
self.template_name = "django_gcp/contrib/unfold/cloud_object_widget.html"
44+
4145
def get_context(self, name, value, attrs):
4246
context = super().get_context(name, value, attrs)
4347

django_gcp/templates/django_gcp/cloud_object_widget.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
<input type="file" class="gcp-file-input" accept="{{ accept_mimetype }}"/>
5252
</label>
5353
{% if not is_required %}
54-
<span class="gcp-clear-selected gcp-button">Clear selection</span>
54+
<span class="gcp-clear-selected gcp-button gcp-button-alert">Clear selection</span>
5555
{% endif %}
5656
</div>
5757

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{% load i18n %}
2+
3+
<div class="gcp-wrapper" data-existing-path="{{ existing_path }}" data-signed-ingress-url="{{ signed_ingress_url }}" data-ingress-path="{{ ingress_path }}">
4+
<div class="border flex flex-col overflow-hidden rounded-md shadow-sm text-sm max-w-2xl dark:border-gray-700">
5+
<input
6+
type="hidden" name="{{ widget.name }}"
7+
{% if widget.value != None and "path" in widget.value %}
8+
value="{{ widget.value|stringformat:'s' }}"
9+
{% else %}
10+
value=""
11+
{% endif %}
12+
{% include "django/forms/widgets/attrs.html" %}
13+
>
14+
15+
<div class="border-b flex flex-col md:flex-row dark:border-gray-700">
16+
<div class="border-b flex flex-row h-9.5 items-center px-3 py-2 w-48 w-full whitespace-nowrap shrink-0 dark:border-gray-700 md:border-0 md:border-r md:w-48">
17+
<span class="material-symbols-outlined mr-2 text-gray-400">
18+
cloud
19+
</span>
20+
21+
{% trans "Existing cloud path" %}
22+
</div>
23+
24+
<div class="flex flex-grow flex-row h-9.5 min-w-0">
25+
<div class="gcp-existing-path flex-grow px-3 py-2 truncate">
26+
{{ existing_path }}
27+
</div>
28+
29+
<div class="gcp-row-controls">
30+
<div class="flex flex-row items-center gap-2 px-2">
31+
{% if existing_path %}
32+
<button type="button" class="gcp-clear-restore-existing bg-red-100 border border-red-200 text-red-500 leading-relaxed rounded-md px-2 shadow-sm whitespace-nowrap dark:bg-red-500/20 dark:border-red-500/20">
33+
{% trans "Clear existing" %}
34+
</button>
35+
{% endif %}
36+
37+
{% if console_url is not None %}
38+
<button type="button" class="gcp-view-console block border leading-relaxed rounded-md px-2 shadow-sm whitespace-nowrap dark:border-gray-700">
39+
{% trans "View in console" %}
40+
</button>
41+
{% endif %}
42+
43+
{% if download_url is not None %}
44+
<button type="button" class="gcp-download block border leading-relaxed rounded-md px-2 shadow-sm whitespace-nowrap dark:border-gray-700" href="{{ download_url }}" download>
45+
{% trans "Download" %}
46+
</button>
47+
{% endif %}
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
53+
<div class="flex flex-col items-center md:flex-row">
54+
<div class="border-b flex flex-row h-9.5 items-center px-3 py-2 w-full whitespace-nowrap shrink-0 md:border-0 md:border-r md:w-48 dark:border-gray-700">
55+
<span class="material-symbols-outlined mr-2 text-gray-400">
56+
cloud_upload
57+
</span>
58+
59+
{% trans "Selected for upload" %}
60+
</div>
61+
62+
<label class="cursor-pointer flex flex-row flex-grow h-9.5 items-center min-w-0 w-full">
63+
<input type="file" class="gcp-file-input" accept="{{ accept_mimetype }}"/>
64+
65+
<span class="gcp-selected-name flex-grow px-3 py-2 truncate"></span>
66+
67+
<span class="gcp-select-label block border leading-relaxed rounded-md mr-2 px-2 shadow-sm whitespace-nowrap dark:border-gray-700">
68+
{% trans "Select file" %}
69+
</span>
70+
</label>
71+
72+
{% if not is_required %}
73+
<span class="gcp-clear-selected border bg-red-100 border-red-200 text-red-500 cursor-pointer leading-relaxed mr-2 rounded-md px-2 shadow-sm whitespace-nowrap dark:bg-red-500/20 dark:border-red-500/20">
74+
{% trans "Clear selection" %}
75+
</span>
76+
{% endif %}
77+
</div>
78+
</div>
79+
</div>
80+
81+
<div class="gcp-overlay">
82+
<p class="gcp-overlay-text">
83+
{% trans "Uploading file, please wait..." %}
84+
</p>
85+
</div>
86+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-gcp"
3-
version = "0.10.6"
3+
version = "0.11.0"
44
description = "Utilities to run Django on Google Cloud Platform"
55
authors = ["Tom Clark"]
66
license = "MIT"

tests/server/example/admin.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from django.contrib import admin
1+
from django.conf import settings
2+
from django.contrib import admin as django_admin
3+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
4+
from django.contrib.auth.models import User
5+
from unfold import admin as unfold_admin
26

37
from tests.server.example.models import (
48
ExampleBlankBlobFieldModel,
@@ -9,28 +13,43 @@
913
)
1014

1115

12-
class ExampleStorageModelAdmin(admin.ModelAdmin):
16+
# Allow switching between unfold and regular, for development purposes, by simply adding unfold to installed apps
17+
if "unfold" in settings.INSTALLED_APPS:
18+
ModelAdmin = unfold_admin.ModelAdmin
19+
django_admin.site.unregister(User)
20+
21+
#
22+
class UserAdmin(BaseUserAdmin, ModelAdmin):
23+
"""Allows working with user admin for unfold and for regular django"""
24+
25+
django_admin.site.register(User, UserAdmin)
26+
27+
else:
28+
ModelAdmin = django_admin.ModelAdmin
29+
30+
31+
class ExampleStorageModelAdmin(ModelAdmin):
1332
"""A basic admin panel to demonstrate normal storage behaviour"""
1433

1534

16-
class ExampleBlobFieldModelAdmin(admin.ModelAdmin):
35+
class ExampleBlobFieldModelAdmin(ModelAdmin):
1736
"""A basic admin panel to demonstrate the direct upload storage behaviour"""
1837

1938

20-
class ExampleBlankBlobFieldModelAdmin(admin.ModelAdmin):
39+
class ExampleBlankBlobFieldModelAdmin(ModelAdmin):
2140
"""A basic admin panel to demonstrate the direct upload storage behaviour where blank=True"""
2241

2342

24-
class ExampleUneditableBlobFieldModelAdmin(admin.ModelAdmin):
43+
class ExampleUneditableBlobFieldModelAdmin(ModelAdmin):
2544
"""A basic admin panel to demonstrate the direct upload storage behaviour where blank=True"""
2645

2746

28-
class ExampleMultipleBlobFieldModelAdmin(admin.ModelAdmin):
47+
class ExampleMultipleBlobFieldModelAdmin(ModelAdmin):
2948
"""A basic admin panel to demonstrate the direct upload storage behaviour where multiple blobfields are used"""
3049

3150

32-
admin.site.register(ExampleStorageModel, ExampleStorageModelAdmin)
33-
admin.site.register(ExampleBlobFieldModel, ExampleBlobFieldModelAdmin)
34-
admin.site.register(ExampleBlankBlobFieldModel, ExampleBlankBlobFieldModelAdmin)
35-
admin.site.register(ExampleUneditableBlobFieldModel, ExampleUneditableBlobFieldModelAdmin)
36-
admin.site.register(ExampleMultipleBlobFieldModel, ExampleMultipleBlobFieldModelAdmin)
51+
django_admin.site.register(ExampleStorageModel, ExampleStorageModelAdmin)
52+
django_admin.site.register(ExampleBlobFieldModel, ExampleBlobFieldModelAdmin)
53+
django_admin.site.register(ExampleBlankBlobFieldModel, ExampleBlankBlobFieldModelAdmin)
54+
django_admin.site.register(ExampleUneditableBlobFieldModel, ExampleUneditableBlobFieldModelAdmin)
55+
django_admin.site.register(ExampleMultipleBlobFieldModel, ExampleMultipleBlobFieldModelAdmin)

tests/server/example/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ def get_destination_path(
5959

6060
# You may wish to add a timestamp, or random string to prevent collisions
6161
# In this case we do the very simple thing of using the original name with random prefix
62-
# If you attempt to overwrite while allow_ovewrite is false, a server error will raise.
63-
# Only set allow_overwrite = True if you really, REALLY, know what you're doing!
6462
random_prefix = str(uuid4())[0:8]
6563

64+
# If you attempt to overwrite while allow_overwrite is false, a server error will raise.
65+
# Only set allow_overwrite = True if you really, REALLY, know what you're doing!
6666
return f"{category}{random_prefix}-{original_name}", allow_overwrite
6767

6868

tests/server/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def get_db_conf():
3434
"django.contrib.auth",
3535
"django.contrib.contenttypes",
3636
"django.contrib.sessions",
37+
"unfold", # <---- COMMENT THIS OUT AND REBOOT SERVER TO DEVELOP ON ORIGINAL DJANGO ADMIN
3738
"django.contrib.admin",
3839
"django.contrib.messages",
3940
"django.contrib.staticfiles",

0 commit comments

Comments
 (0)