Django Admin - add collapse to a fieldset, but have it start expanded - django

Is there a way to make a fieldset collapsible, but start expanded? When you add collapse to the fieldset classes, it gets the functionality but starts collapsed. I've taken a look at the JS that shows/hides the fieldset content, but it doesn't look like there's anything in there to do what I'd like it to, so I'm assuming I'll have to roll my own. Just wanted to check before I went through that effort.

admin.py:
class PageAdmin(admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('title', 'content', )
}),
('Other Informations', {
'classes': ('collapse', 'open'),
'fields': ('slug', 'create-date',)
}),
)
templates/app_label/model_name/change_form.html:
{% extends "admin/model_name/change_form.html" %}
{% block extrahead %}
{{ block.super }}
<script src="{{ STATIC_URL }}admin/js/collapse-open.js" type="text/javascript"></script>
{% endblock %}
static/admin/js/collapse-open.js:
(function($) {
$(document).ready(function() {
$('fieldset.collapse.open').removeClass('collapsed');
});
})(django.jQuery);

I know this is real old but I also just came across this issue. After thinking about it way too hard I found an easy solution which seems to get the job done involving 0 plugins or additional js.
Within fieldsets construct Add 'collapse in' rather than 'collapse' to class:
fieldsets = [
('Start Expanded', {
'fields': ['field1', 'field2', 'field3'],
'classes': ['collapse in',]
})
]

django-grappelli provides this as one of the features. Here's the wiki page about that feature (with screenshots).

Accoding with the grappelli docs only need to add "classes" : ('grp-collapse grp-closed')
for example
class EntryOptions(admin.ModelAdmin):
...
fieldsets = (
('', {
'fields': ('title', 'subtitle', 'slug', 'pub_date', 'status',),
}),
('Flags', {
'classes': ('grp-collapse grp-closed',),
'fields' : ('flag_front', 'flag_sticky', 'flag_allow_comments',),
}),
note: if you use grappelli version < 2.4 use 'grp-closed' instead 'collapse-closed'*
actually 'collapse-close' still is working but is recommended to use the new classes

Starting from Setomidor answer, I'd like to suggest a simpler alternative that does exactly what you want (if Grappelli is not an option, obviously).
Create the template override as explained (admin/(app)/(model)/change_form.html) and instead of removing the collapsible effect for the "add" model case, simply call the click method of the field-set collapser (i.e. the small link with the show/hide text) to have the whole field-set expanded as soon as the page loads.

The quickest hack I could find was to add a new class start-open to the fieldset
classes = ['collapse', 'start-open']
and then modify static/admin/js/collapse.js.
from:
// Add toggle to anchor tag
var toggles = document.querySelectorAll('fieldset.collapse a.collapse-toggle');
var toggleFunc = function(ev) {
ev.preventDefault();
var fieldset = closestElem(this, 'fieldset');
if (fieldset.classList.contains('collapsed')) {
// Show
this.textContent = gettext('Hide');
fieldset.classList.remove('collapsed');
} else {
// Hide
this.textContent = gettext('Show');
fieldset.classList.add('collapsed');
}
};
for (i = 0; i < toggles.length; i++) {
toggles[i].addEventListener('click', toggleFunc);
}
to:
// Add toggle to anchor tag
var toggles = document.querySelectorAll('fieldset.collapse a.collapse-toggle');
// NEW: select toggles to open
var open_toggles = document.querySelectorAll('fieldset.collapse.start-open a.collapse-toggle');
var toggleFunc = function(ev) {
ev.preventDefault();
var fieldset = closestElem(this, 'fieldset');
if (fieldset.classList.contains('collapsed')) {
// Show
this.textContent = gettext('Hide');
fieldset.classList.remove('collapsed');
} else {
// Hide
this.textContent = gettext('Show');
fieldset.classList.add('collapsed');
}
};
for (i = 0; i < toggles.length; i++) {
toggles[i].addEventListener('click', toggleFunc);
}
// NEW: open toggles
for (i = 0; i < open_toggles.length; i++) {
toggles[i].click();
}

Old question, but I ran into the same one and came up with an answer which can be implemented using standard django:
create file: admin/(app)/(model)/change_form.html in your templates directory to make your (model) of your (app) use that form file.
Add this code to the file:
{% extends "admin/change_form.html" %}
{% block extrahead %}
<!-- Load superblock (where django.jquery resides) -->
{{ block.super }}
<!-- only do this on 'add' actions (and not 'change' actions) -->
{% if add and adminform %}
<script type="text/javascript">
(function($) {
$(document).ready(function($) {
//Remove the 'collapsed' class to make the fieldset open
$('.collapse').removeClass('collapsed');
//Remove the show/hide links
$('.collapse h2 a').remove();
//Tidy up by removing the parenthesis from all headings
var $headings = $(".collapse h2");
$headings.each(function(i, current){
var str = $(current).text();
parenthesisStart = str.indexOf('(');
$(current).text(str.substring(0, parenthesisStart));
});
});
})(django.jQuery);
</script>
{% endif %}
{% endblock %}
For more information about using django.jQuery instead of "regular" jQuery, see: http://blog.picante.co.nz/post/Customizing-Django-Admin-with-jQuery--Getting--is-not-a-function/

So this one worked for me:
class PageAdmin(admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('title', 'content', )
}),
('Other Informations', {
'classes': ('wide'),
'fields': ('slug', 'create-date',)
}),
)

With django 4.x, here is my admin/js/collapse.js
add start-open in class
'use strict';
{
window.addEventListener('load', function() {
// Add anchor tag for Show/Hide link
const fieldsets = document.querySelectorAll('fieldset.collapse');
// NEW: select toggles to open
var open_toggles = document.querySelectorAll('fieldset.collapse.start-open');
for (const [i, elem] of fieldsets.entries()) {
// Don't hide if fields in this fieldset have errors
if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
if (elem.classList[3] != 'start-open') {
elem.classList.add('collapsed');
}
const h2 = elem.querySelector('h2');
const link = document.createElement('a');
link.id = 'fieldsetcollapser' + i;
link.className = 'collapse-toggle';
link.href = '#';
if (elem.classList[3] != 'start-open') {
link.textContent = gettext('Show');
} else {
link.textContent = gettext('Hide');
}
h2.appendChild(document.createTextNode(' ('));
h2.appendChild(link);
h2.appendChild(document.createTextNode(')'));
}
}
// Add toggle to hide/show anchor tag
const toggleFunc = function(ev) {
if (ev.target.matches('.collapse-toggle')) {
ev.preventDefault();
ev.stopPropagation();
const fieldset = ev.target.closest('fieldset');
if (fieldset.classList.contains('collapsed')) {
// Show
ev.target.textContent = gettext('Hide');
fieldset.classList.remove('collapsed');
} else {
// Hide
ev.target.textContent = gettext('Show');
fieldset.classList.add('collapsed');
}
}
};
document.querySelectorAll('fieldset.module').forEach(function(el) {
el.addEventListener('click', toggleFunc);
});
});
}

Related

Django template tag URL is not working in JavaScript

I want to add an edit button and delete image dynamically to table, but it is showing error
Page Not Found at
Request URL: http://127.0.0.1:8000/%7B%25%20url%20'expense-edit'%20expense.id%20%25%7D
here is js file
const searchField = document.querySelector("#searchField");
const tableOutput = document.querySelector(".table-output");
const appTable = document.querySelector(".app-table");
const paginationContainer = document.querySelector(".pagination-container");
tableOutput.style.display = 'none';
const noResults = document.querySelector(".no-results");
const tbody = document.querySelector(".table-body");
searchField.addEventListener('keyup', (e) => {
const searchValue = e.target.value;
if (searchValue.trim().length > 0) {
paginationContainer.style.display = "none";
tbody.innerHTML = "";
fetch("http://127.0.0.1:8000/search-expenses", {
body: JSON.stringify({ searchText: searchValue }),
method: "POST",
})
.then((res) => res.json())
.then((data) => {
console.log("data", data);
appTable.style.display = "none";
tableOutput.style.display = "block";
console.log("data.length", data.length);
if (data.length === 0) {
noResults.style.display = "block";
tableOutput.style.display = "none";
}
else {
noResults.style.display = "none";
data.forEach((item) => {
tbody.innerHTML += `
<tr>
<td>${item.amount}</td>
<td>${item.category}</td>
<td>${item.description}</td>
<td>${item.date}</td>
<td>Edit<img src="{% static 'img/delete.png' %}" width="35" height="35"/></td>
</tr>`;
});
}
});
}
else {
noResults.style.display = "none";
tableOutput.style.display = "none";
appTable.style.display = "block";
paginationContainer.style.display = "block";
}
});
urls.py
from django.urls import path
from . import views
from django.views.decorators.csrf import csrf_exempt
urlpatterns = [
path('', views.home, name="home"),
path('expenses', views.index, name='expenses'),
path('add-expenses', views.add_expenses, name='add-expenses'),
path('edit-expense/<int:id>', views.expense_edit, name='expense-edit'),
path('expense-delete/<int:id>', views.delete_expense, name='expense-delete'),
path('search-expenses', csrf_exempt(views.search_expenses), name='search_expenses'),
path('expense_category_summary', views.expense_category_summary, name="expense_category_summary"),
path('stats', views.stats_view, name="stats"),
path('export_csv', views.export_csv, name="export-csv"),
path('export_excel', views.export_excel, name="export-excel"),
path('export_pdf', views.export_pdf, name="export-pdf"),
]
I want to add a button that has a link to the next page through JavaScript using the Django URL template tag. thanks in advance
When you write {% url 'expense-edit' expense.id %}, you're using a Django-specific "language" called Django template language which means that the mentioned syntax can only be "understood" by Django templates.
What you're doing here is loading a separate JavaScript file in which the Django template syntax simply won't work because when browser comes to the point to evaluate that file, {% url 'expense-edit' expense.id %} will look just another string literal, and not what you would expect to.
It should work if you inline your JavaScript example directly into the Django template.
So there is two possible Hacks to do this:
One is putting the written script in your tenplate file as:
<script type="text/javascript">
const searchField = document.querySelector("#searchField");
const tableOutput = document.querySelector(".table-output");
...
</script>
The issue with this approach is the scope of variables as you may not want to declare things globally so it could be considered an easy approach, but not necessarily the best solution.
So the other solution is to change the name of file from script.js to script.js.html and include in your desired template as :
...
{% include "my_app/script.js.html" %}
Instead of this:
Edit<img src="{% static 'img/delete.png' %}" width="35" height="35"/></td>
Try this way:
Edit
<img src="{% static 'img/delete.png' %}" width="35" height="35"/></td>

Pass javascript variable to views.py

I really need some help on this subject, which seems easy, but i cant get myself to figure out the problem.
I understand that using ajax is the best way to pass a variable from a template to a view.
Is what i have done good, or would you recommend i try something different ?
function updateArea(e) {
var data = draw.getAll();
var answer = document.getElementById('calculated-area');
if (data.features.length > 0) {
var area = turf.area(data);
// restrict to area to 2 decimal points
var rounded_area = Math.round(area*100)/100;
answer.innerHTML = '<p><strong>' + rounded_area + '</strong></p><p>square meters</p>';
var convertedData = 'text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(data));
document.getElementById('export').setAttribute('href', 'data:' + convertedData);
document.getElementById('export').setAttribute('download','data.geojson');
} else {
answer.innerHTML = '';
if (e.type !== 'draw.delete') alert("Use the draw tools to draw a polygon!");
}
}
I want to take the area variable and convertedData variable and pass them to a view.
So after some searching and trial and error, here is what I tried to do using Ajax.
When I run this i get no errors, but i get nothing in my database.
template.html
<form id="areaform" method="POST">
{% csrf_token %}
<input type="submit" name="area" value=""/>
</form>
$(document).on('submit', '#areaform',function(e) {
e.preventDefault();
$.ajax({
type: 'POST',
url:'fertisatmap/area',
data:{
name: $('#area').val(),
},
sucess:function(){}
});
});
urls.py
path('fertisatmap/area', fertisat_views.fertisatareaview, name='fertisat-area'),
views.py
def fertisatareaview(request):
if request.method == "POST":
area = request.POST['area']
Fertidb.objects.create(
area = area)
return HttpResponse('')

Sitecore Viewrendering with dynamic datasource

Hi I have simple view rendering (e.g. with title and body).
While it works perfectly fine when i give a datasource to my rendering control in presentation layout - I am wondering if I can do it by code - i.e. define the data datasource by code.
Currently I have something like this that works just fine:
#inherits Glass.Mapper.Sc.Web.Mvc.GlassView<sample.Web.Models.sampleclass>
#if (Model != null)
{
<div>
#Model.Title
</div>
}
Looking for something like below where i can define my datasource or the item
#inherits Glass.Mapper.Sc.Web.Mvc.GlassView<sample.Web.Models.sampleclass>
#datasource = Sitecore.context.database.getitem("some different path or id");
#if (Model != null)
{
<div>
#Model.Title
</div>
}
You can use something like this:
#{
var dynamicDatasource = new SitecoreContext().GetItem<sampleclass>(other_item_id)
}
#if (dynamicDatasource != null)
{
<div>
#Html.Glass().Editable(dynamicDatasource, d => d.Title)
</div>
}

Rails or Ember object model breaking browser's pushState functionality?

** I'm using Ember.Object instead of Ember Data, to pull data from an api and I believe that might be causing the issue.**
I have my resources nested as so:
Mdm.Router.map ->
#resource "groups", ->
#resource "group", path: ':group_id'
'/groups/ loads a list of all groups on the left side of the browser. Each group is linking to its specific group_id. When clicked, the 'group' template renders on the right side of the screen, showing details of one group while the list remains to the left.
However, when you click the back button, or manually enter the group_id into the url the individual groups dont render. The url will update in the browser window, but the content wont change to match it.
I have the singular 'group' template rendering inside of the 'groups' template with the {{outlet}}.
My groups_route.js.coffee looks like this:
Mdm.GroupsRoute = Ember.Route.extend(model: ->
Mdm.Group.all()
)
application.hbs:
<div class="container">
<div class="nav-bar">
<img src="assets/logo_loginbox.png" class="logo">
<ul class="nav-menu">
<li>GROUPS</li>
<li>USERS</li>
</ul>
</div><!-- nav-bar -->
<hr>
{{outlet}}
</div><!-- container -->
groups.hbs:
<h1>Groups</h1>
{{ partial groupsList }}
<div class="group">
{{outlet}}
</div><!-- group -->
group.hbs:
<h1>{{name}}</h1>
I'm getting the following error in the console when I use the back button or try to load the page with the group_id present:
Uncaught TypeError: Object function () {
if (!wasApplied) {
Class.proto(); // prepare prototype...
}
o_defineProperty(this, GUID_KEY, undefinedDescriptor);
o_defineProperty(this, '_super', undefinedDescriptor);
var m = meta(this);
m.proto = this;
if (initMixins) {
// capture locally so we can clear the closed over variable
var mixins = initMixins;
initMixins = null;
this.reopen.apply(this, mixins);
}
if (initProperties) {
// capture locally so we can clear the closed over variable
var props = initProperties;
initProperties = null;
var concatenatedProperties = this.concatenatedProperties;
for (var i = 0, l = props.length; i < l; i++) {
var properties = props[i];
Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin));
for (var keyName in properties) {
if (!properties.hasOwnProperty(keyName)) { continue; }
var value = properties[keyName],
IS_BINDING = Ember.IS_BINDING;
if (IS_BINDING.test(keyName)) {
var bindings = m.bindings;
if (!bindings) {
bindings = m.bindings = {};
} else if (!m.hasOwnProperty('bindings')) {
bindings = m.bindings = o_create(m.bindings);
}
bindings[keyName] = value;
}
var desc = m.descs[keyName];
Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty));
Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1));
if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) {
var baseValue = this[keyName];
if (baseValue) {
if ('function' === typeof baseValue.concat) {
value = baseValue.concat(value);
} else {
value = Ember.makeArray(baseValue).concat(value);
}
} else {
value = Ember.makeArray(value);
}
}
if (desc) {
desc.set(this, keyName, value);
} else {
if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) {
this.setUnknownProperty(keyName, value);
} else if (MANDATORY_SETTER) {
Ember.defineProperty(this, keyName, null, value); // setup mandatory setter
} else {
this[keyName] = value;
}
}
}
}
}
finishPartial(this, m);
delete m.proto;
finishChains(this);
this.init.apply(this, arguments);
} has no method 'find' application.js:51233
Mdm.GroupRoute.Ember.Route.extend.model application.js:51233
superWrapper application.js:12849
Ember.Route.Ember.Object.extend.deserialize application.js:36503
collectObjects application.js:35614
proceed application.js:35638
(anonymous function) application.js:1193
fire application.js:1038
self.fireWith application.js:1149
(anonymous function) application.js:1200
fire application.js:1038
self.fireWith application.js:1149
done application.js:8075
script.onload.script.onreadystatechange

CheckBoxList multiple selections: how to model bind back and get all selections?

This code:
Html.CheckBoxList(ViewData.TemplateInfo.HtmlFieldPrefix, myList)
Produces this mark-up:
<ul><li><input name="Header.h_dist_cd" type="checkbox" value="BD" />
<span>BD - Dist BD Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="SS" />
<span>SS - Dist SS Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="DS" />
<span>DS - Dist DS Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="SW" />
<span>SW - Dist SW Name </span></li>
</ul>
You can check multiple selections. The return string parameter Header.h_dist_cd only contains the first value selected. What do I need to do to get the other checked values?
The post method parameter looks like this:
public ActionResult Edit(Header header)
I'm assuming that Html.CheckBoxList is your extension and that's markup that you generated.
Based on what you're showing, two things to check:
The model binder is going to look for an object named Header with string property h_dist_cd to bind to. Your action method looks like Header is the root view model and not a child object of your model.
I don't know how you are handling the case where the checkboxes are cleared. The normal trick is to render a hidden field with the same name.
Also a nit, but you want to use 'label for="..."' so they can click the text to check/uncheck and for accessibility.
I've found that using extensions for this problem is error prone. You might want to consider a child view model instead. It fits in better with the EditorFor template system of MVC2.
Here's an example from our system...
In the view model, embed a reusable child model...
[AtLeastOneRequired(ErrorMessage = "(required)")]
public MultiSelectModel Cofamilies { get; set; }
You can initialize it with a standard list of SelectListItem...
MyViewModel(...)
{
List<SelectListItem> initialSelections = ...from controller or domain layer...;
Cofamilies = new MultiSelectModel(initialSelections);
...
The MultiSelectModel child model. Note the setter override on Value...
public class MultiSelectModel : ICountable
{
public MultiSelectModel(IEnumerable<SelectListItem> items)
{
Items = new List<SelectListItem>(items);
_value = new List<string>(Items.Count);
}
public int Count { get { return Items.Count(x => x.Selected); } }
public List<SelectListItem> Items { get; private set; }
private void _Select()
{
for (int i = 0; i < Items.Count; i++)
Items[i].Selected = Value[i] != "false";
}
public List<SelectListItem> SelectedItems
{
get { return Items.Where(x => x.Selected).ToList(); }
}
private void _SetSelectedValues(IEnumerable<string> values)
{
foreach (var item in Items)
{
var tmp = item;
item.Selected = values.Any(x => x == tmp.Value);
}
}
public List<string> SelectedValues
{
get { return SelectedItems.Select(x => x.Value).ToList(); }
set { _SetSelectedValues(value); }
}
public List<string> Value
{
get { return _value; }
set { _value = value; _Select(); }
}
private List<string> _value;
}
Now you can place your editor template in Views/Shared/MultiSelectModel.ascx...
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<WebUI.Cofamilies.Models.Shared.MultiSelectModel>" %>
<div class="set">
<%=Html.LabelFor(model => model)%>
<ul>
<% for (int i = 0; i < Model.Items.Count; i++)
{
var item = Model.Items[i];
string name = ViewData.ModelMetadata.PropertyName + ".Value[" + i + "]";
string id = ViewData.ModelMetadata.PropertyName + "_Value[" + i + "]";
string selected = item.Selected ? "checked=\"checked\"" : "";
%>
<li>
<input type="checkbox" name="<%= name %>" id="<%= id %>" <%= selected %> value="true" />
<label for="<%= id %>"><%= item.Text %></label>
<input type="hidden" name="<%= name %>" value="false" />
</li>
<% } %>
</ul>
<%= Html.ValidationMessageFor(model => model) %>
Two advantages to this approach:
You don't have to treat the list of items separate from the selection value. You can put attributes on the single property (e.g., AtLeastOneRequired is a custom attribute in our system)
you separate model and view (editor template). We have a horizontal and a vertical layout of checkboxes for example. You could also render "multiple selection" as two listboxes with back and forth buttons, multi-select list box, etc.
I think what you need is how gather selected values from CheckBoxList that user selected and here is my solution for that:
1- Download Jquery.json.js and add it to your view as reference:
2- I've added a ".cssMyClass" to all checkboxlist items so I grab the values by their css class:
<script type="text/javascript" >
$(document).ready(function () {
$("#btnSubmit").click(sendValues);
});
function populateValues()
{
var data = new Array();
$('.myCssClas').each(function () {
if ($(this).attr('checked')) {
var x = $(this).attr("value");
data.push(x);
}
});
return data;
}
function sendValues() {
var data = populateValues();
$.ajax({
type: 'POST',
url: '#Url.Content("~/Home/Save")',
data: $.json.encode(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function () { alert("1"); }
});
}
</script>
3- As you can see I've added all selected values to an Array and I've passed it to "Save" action of "Home" controller by ajax 4- in Controller you can receive the values by adding an array as argument:
[HttpPost]
public ActionResult Save(int[] val)
{
I've searched too much but apparently this is the only solution. Please let me know if you find a better solution for it.
when you have multiple items with the same name you will get their values separated with coma