Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editable relations support with dropdown #61

Merged
merged 5 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions admin/templates/resource/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,32 @@
{% for attribute in editable_attributes %}
<div class="w-1/2 mb-4">
<div class="flex flex-col mr-3">
<label for={{ attribute.name }} class="uppercase text-gray-500 text-xs mb-1">{{ attribute.name | format_label }}</label>
{% if 'VARCHAR' in attribute.type %}
<input type="text" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif attribute.type == 'INTEGER' %}
<input type="number" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif attribute.type == 'BOOLEAN' %}
<select name={{ attribute.name }} class="py-2.5 px-3 border border-gray-300 rounded">
<option value="True">True</option>
<option value="False">False</option>
{% if attribute["name"] in editable_relations: %}
<label for={{ attribute.name }} class="uppercase text-gray-500 text-xs mb-1">{{ editable_relations[attribute["name"]]['label'] | format_label }}</label>
<select name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2.5 px-3 border border-gray-300 rounded">
<option value="">Select value</option>
{% for related_attribute in editable_relations[attribute["name"]]['options'] %}
<option value="{{ related_attribute.value }}">{{ related_attribute.label }}</option>
{% endfor %}
</select>
{% elif attribute.type == 'TEXT' or attribute.type == 'JSON' %}
<textarea class="py-2 px-3 border border-gray-300 rounded" rows="5" name="{{ attribute.name }}" id="{{ attribute.name }}"></textarea>
{% elif attribute.type == 'DATETIME' %}
<input type="datetime-local" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif attribute.type == 'DATE' %}
<input type="date" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% else %}
<label for={{ attribute.name }} class="uppercase text-gray-500 text-xs mb-1">{{ attribute.name | format_label }}</label>
{% if 'VARCHAR' in attribute.type %}
<input type="text" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif attribute.type == 'INTEGER' %}
<input type="number" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif attribute.type == 'BOOLEAN' %}
<select name={{ attribute.name }} class="py-2.5 px-3 border border-gray-300 rounded">
<option value="True">True</option>
<option value="False">False</option>
</select>
{% elif attribute.type == 'TEXT' or attribute.type == 'JSON' %}
<textarea class="py-2 px-3 border border-gray-300 rounded" rows="5" name="{{ attribute.name }}" id="{{ attribute.name }}"></textarea>
{% elif attribute.type == 'DATETIME' %}
<input type="datetime-local" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif attribute.type == 'DATE' %}
<input type="date" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% endif %}
{% endif %}
</div>
</div>
Expand Down
44 changes: 27 additions & 17 deletions admin/templates/resource/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,34 @@
{% for attribute in editable_attributes %}
<div class="w-1/2 mb-4">
<div class="flex flex-col mr-3">
<label for={{ attribute.name }} class="uppercase text-gray-500 text-xs mb-1">{{ attribute.name | format_label }}</label>
{% if attribute["name"] == admin_configs['user']['secret'] %}
<input type="password" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif 'VARCHAR' in attribute.type %}
<input type="text" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] }}" {% endif %} />
{% elif attribute.type == 'INTEGER' or attribute.type == 'BIGINT' %}
<input type="number" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] }}" {% endif %} />
{% elif attribute.type == 'BOOLEAN' %}
<select name={{ attribute.name }} class="py-2.5 px-3 border border-gray-300 rounded">
<option value="True" {% if resource[attribute.name] == True %} selected="selected "{% endif %}>True</option>
<option value="False" {% if resource[attribute.name] == False %} selected="selected "{% endif %}>False</option>
{% if attribute["name"] in editable_relations: %}
<label for={{ attribute.name }} class="uppercase text-gray-500 text-xs mb-1">{{ editable_relations[attribute["name"]]['label'] | format_label }}</label>
<select name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2.5 px-3 border border-gray-300 rounded">
<option value="">Select value</option>
{% for related_attribute in editable_relations[attribute["name"]]['options'] %}
<option value="{{ related_attribute.value }}" {% if related_attribute.value == resource[attribute.name] %} selected="selected "{% endif %} data-related_attribute="{{related_attribute.value}}" data-value="{{resource[attribute.name]}}">{{ related_attribute.label }}</option>
{% endfor %}
</select>
{% elif attribute.type == 'TEXT' or attribute.type == 'JSON' %}
<textarea class="py-2 px-3 border border-gray-300 rounded" rows="5" name="{{ attribute.name }}" id="{{ attribute.name }}">{% if resource[attribute.name] %}{{ resource[attribute.name] }}{% endif %}</textarea>
{% elif attribute.type == 'DATETIME' %}
<input type="datetime-local" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] | admin_format_datetime }}" {% endif %} />
{% elif attribute.type == 'DATE' %}
<input type="date" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] | admin_format_datetime }}" {% endif %} />
{% else %}
<label for={{ attribute.name }} class="uppercase text-gray-500 text-xs mb-1">{{ attribute.name | format_label }}</label>
{% if attribute["name"] == admin_configs['user']['secret'] %}
<input type="password" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" />
{% elif 'VARCHAR' in attribute.type %}
<input type="text" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] }}" {% endif %} />
{% elif attribute.type == 'INTEGER' or attribute.type == 'BIGINT' %}
<input type="number" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] }}" {% endif %} />
{% elif attribute.type == 'BOOLEAN' %}
<select name={{ attribute.name }} class="py-2.5 px-3 border border-gray-300 rounded">
<option value="True" {% if resource[attribute.name] == True %} selected="selected "{% endif %}>True</option>
<option value="False" {% if resource[attribute.name] == False %} selected="selected "{% endif %}>False</option>
</select>
{% elif attribute.type == 'TEXT' or attribute.type == 'JSON' %}
<textarea class="py-2 px-3 border border-gray-300 rounded" rows="5" name="{{ attribute.name }}" id="{{ attribute.name }}">{% if resource[attribute.name] %}{{ resource[attribute.name] }}{% endif %}</textarea>
{% elif attribute.type == 'DATETIME' %}
<input type="datetime-local" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] | admin_format_datetime }}" {% endif %} />
{% elif attribute.type == 'DATE' %}
<input type="date" name="{{ attribute.name }}" id="{{ attribute.name }}" class="py-2 px-3 border border-gray-300 rounded" {% if resource[attribute.name] %} value="{{ resource[attribute.name] | admin_format_datetime }}" {% endif %} />
{% endif %}
{% endif %}
</div>
</div>
Expand Down
12 changes: 6 additions & 6 deletions admin/templates/resource/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ <h1>You do not have permission to view this link. Please reach out to support if
{% else %}
<div class="text-3xl mb-4">Manage {{ resource_type | admin_label_plural }}</div>
<div class="flex justify-between">
{% if permissions.get(resource_type).get('create'): %}
<a href={{ url_for('.resource_create', resource_type=resource_type) }} class="bg-blue-500 hover:bg-blue-300 text-white font-bold py-2 px-4 rounded flex items-center">
Create {{ resource_type | admin_label_singular }}
</a>
{% endif %}
<div class="flex">
{% if permissions.get(resource_type).get('create'): %}
<a href={{ url_for('.resource_create', resource_type=resource_type) }} class="bg-blue-500 hover:bg-blue-300 text-white font-bold py-2 px-4 rounded flex items-center">
Create {{ resource_type | admin_label_singular }}
</a>
{% endif %}
{% if permissions.get(resource_type): %}
<form action={{ url_for('.resource_list', resource_type=resource_type) }} method="GET">
<div class="flex">
<div class="flex ml-2">
{% if not hide_search %}
<div class="mr-2 flex">
<input type="text" name="search" id="search" value="{{ search_params.search_query }}" placeholder="Search..." class="border border-gray-300 text-black text-sm py-2 px-4 rounded">
Expand Down
28 changes: 27 additions & 1 deletion admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,12 +713,13 @@ def resource_create(resource_type):
resource_class = get_resource_class(resource_type)
model = resource_class.model
editable_attributes = get_editable_attributes(resource_type)

editable_relations = get_editable_relations(resource_class)
if request.method == "GET":
return render_template(
"resource/create.html",
resource_type=resource_type,
editable_attributes=editable_attributes,
editable_relations=editable_relations,
)

attributes_to_save = {}
Expand Down Expand Up @@ -836,13 +837,15 @@ def resource_edit(resource_type, resource_id):
resource
) # make a clone before there are any updates

editable_relations = get_editable_relations(resource_class)
if request.method == "GET":
return render_template(
"resource/edit.html",
resource_type=resource_type,
resource=resource,
editable_attributes=editable_attributes,
admin_configs=admin_configs,
editable_relations=editable_relations,
)

if (
Expand Down Expand Up @@ -1200,6 +1203,29 @@ def get_preprocess_data(pagination, list_display):
return processed_data


def get_editable_relations(resource_class):
editable_relations = {}
if hasattr(resource_class, "editable_relations_dropdown"):
for editable_relation in resource_class.editable_relations_dropdown:
attribute_key = editable_relation["key"]
related_model = editable_relation["related_model"]
related_label = editable_relation["related_label"]
related_key = editable_relation["related_key"]
related_data = related_model.query.order_by(related_label).all()
editable_relations[attribute_key] = {}
editable_relations[attribute_key]["label"] = editable_relation[
"label"
]
editable_relations[attribute_key]["options"] = [
{
"label": getattr(data, related_label),
"value": getattr(data, related_key),
}
for data in related_data
]
return editable_relations


@admin.route("/update_approval_status", methods=["POST"])
def update_receipt_status():
response = update_approval_status(current_user)
Expand Down
22 changes: 22 additions & 0 deletions docs/ADMIN_VIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,25 @@ class UserAdmin(FlaskAdmin):
list_display = ('name', 'phone_number')
protected_attributes = ['last_active_date', 'token_expires_at']
```

### Related attributes as dropdown
There can be related models that you would prefer to select by their label or name instead of putting a finding the corresponding id or primary key value and putting it correctly in the text field. Dropdowns are handy. For example, you would prefer to select a language as a dropdown for a user instead of looking through languages table, finding the corresponding language id and putting it in the language id field when creating or editing a user. This is where, related attributes come in to save the day.

Defining editable related attributes are pretty straightforward. In you class, you can put in a list/array like this:
```py
class UserAdmin(FlaskAdmin):
model = User
name = 'user'
list_display = ('name', 'phone_number')
editable_relations_dropdown = [
{
"key": "language_id", # the foreign key field you wish to replace with the dropdown
"label": "language", # the label of the field
"related_model": LanguageModel, # the related model
"related_label": "name", # the related model field that will display as label of each dropdown option
"related_key": "id", # the related model primary key field for each dropdown option value
}
]
```

All the editable relations defined through this attribute will replace the corresponding foreign key field with a dropdown having options defined. This will impact both create resource page and edit resource page, where in edit resource, the existing option will come pre-selected.