1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 01:48:21 +02:00

Add django-bootstrap-datepicker-plus and django-colorfield, move statics

This commit is contained in:
Alexandre Iooss
2020-08-09 18:51:20 +02:00
parent 8c1cf754ed
commit 53b496546d
213 changed files with 5 additions and 36891 deletions

View File

@ -0,0 +1,37 @@
$("#alias_input").on('keypress',function(e) {
if(e.which == 13) {
$("#alias_submit").click();
}
});
function create_alias(note_id){
$.post("/api/note/alias/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"name": $("#alias_input").val(),
"note": note_id
}
).done(function(){
$("#alias_table").load(location.pathname+ " #alias_table");
addMsg("Alias ajouté","success");
})
.fail(function(xhr, textStatus, error){
errMsg(xhr.responseJSON);
});
}
// on click of button "delete" , call the API
function delete_button(button_id){
$.ajax({
url:"/api/note/alias/"+button_id+"/",
method:"DELETE",
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
})
.done(function(){
addMsg('Alias supprimé','success');
$("#alias_table").load(location.pathname + " #alias_table");
})
.fail(function(xhr,textStatus, error){
errMsg(xhr.responseJSON);
});
}

View File

@ -0,0 +1,57 @@
$(document).ready(function () {
$(".autocomplete").keyup(function(e) {
let target = $("#" + e.target.id);
let prefix = target.attr("id");
let api_url = target.attr("api_url");
let api_url_suffix = target.attr("api_url_suffix");
if (!api_url_suffix)
api_url_suffix = "";
let name_field = target.attr("name_field");
if (!name_field)
name_field = "name";
let input = target.val();
target.addClass("is-invalid");
target.removeClass("is-valid");
$("#" + prefix + "_reset").removeClass("d-none");
$.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) {
let html = "";
objects.results.forEach(function (obj) {
html += li(prefix + "_" + obj.id, obj[name_field]);
});
let results_list = $("#" + prefix + "_list");
results_list.html(html);
objects.results.forEach(function (obj) {
$("#" + prefix + "_" + obj.id).click(function() {
target.val(obj[name_field]);
$("#" + prefix + "_pk").val(obj.id);
results_list.html("");
target.removeClass("is-invalid");
target.addClass("is-valid");
if (typeof autocompleted != 'undefined')
autocompleted(obj, prefix)
});
if (input === obj[name_field])
$("#" + prefix + "_pk").val(obj.id);
});
if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) {
results_list.children().first().trigger("click");
}
});
});
$(".autocomplete-reset").click(function() {
let name = $(this).attr("id").replace("_reset", "");
$("#" + name + "_pk").val("");
$("#" + name).val("");
$("#" + name + "_list").html("");
$(this).addClass("d-none");
});
});

388
note_kfet/static/js/base.js Normal file
View File

@ -0,0 +1,388 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Convert balance in cents to a human readable amount
* @param value the balance, in cents
* @returns {string}
*/
function pretty_money(value) {
if (value % 100 === 0)
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €";
else
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "."
+ (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €";
}
/**
* Add a message on the top of the page.
* @param msg The message to display
* @param alert_type The type of the alert. Choices: info, success, warning, danger
* @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored.
*/
function addMsg(msg, alert_type, timeout = -1) {
let msgDiv = $("#messages");
let html = msgDiv.html();
let id = Math.floor(10000 * Math.random() + 1);
html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" +
"<button id=\"close-message-" + id + "\" class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>"
+ msg + "</div>\n";
msgDiv.html(html);
if (timeout > 0) {
setTimeout(function () {
$("#close-message-" + id).click();
}, timeout);
}
}
/**
* add Muliple error message from err_obj
* @param errs_obj [{error_code:erro_message}]
* @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored.
*/
function errMsg(errs_obj, timeout = -1) {
for (const err_msg of Object.values(errs_obj)) {
addMsg(err_msg, 'danger', timeout);
}
}
var reloadWithTurbolinks = (function () {
var scrollPosition;
function reload() {
scrollPosition = [window.scrollX, window.scrollY];
Turbolinks.visit(window.location.toString(), {action: 'replace'})
}
document.addEventListener('turbolinks:load', function () {
if (scrollPosition) {
window.scrollTo.apply(window, scrollPosition);
scrollPosition = null
}
});
return reload;
})();
/**
* Reload the balance of the user on the right top corner
*/
function refreshBalance() {
$("#user_balance").load("/ #user_balance");
}
/**
* Query the 20 first matched notes with a given pattern
* @param pattern The pattern that is queried
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/
function getMatchedNotes(pattern, fun) {
$.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun);
}
/**
* Generate a <li> entry with a given id and text
*/
function li(id, text, extra_css) {
return "<li class=\"list-group-item py-1 px-2 d-flex justify-content-between align-items-center text-truncate "
+ (extra_css ? extra_css : "") + "\"" + " id=\"" + id + "\">" + text + "</li>\n";
}
/**
* Return style to apply according to the balance of the note and the validation status of the email address
* @param note The concerned note.
*/
function displayStyle(note) {
if (!note)
return "";
let balance = note.balance;
var css = "";
if (balance < -5000)
css += " text-danger bg-dark";
else if (balance < -1000)
css += " text-danger";
else if (balance < 0)
css += " text-warning";
else if (!note.email_confirmed)
css += " text-white bg-primary";
else if (note.membership && note.membership.date_end < new Date().toISOString())
css += "text-white bg-info";
return css;
}
/**
* Render note name and picture
* @param note The note to render
* @param alias The alias to be displayed
* @param user_note_field
* @param profile_pic_field
*/
function displayNote(note, alias, user_note_field = null, profile_pic_field = null) {
if (!note.display_image) {
note.display_image = '/media/pic/default.png';
}
let img = note.display_image;
if (alias !== note.name && note.name)
alias += " (aka. " + note.name + ")";
if (user_note_field !== null) {
$("#" + user_note_field).removeAttr('class');
$("#" + user_note_field).addClass(displayStyle(note));
$("#" + user_note_field).text(alias + (note.balance == null ? "" : (" :\n" + pretty_money(note.balance))));
if (profile_pic_field != null) {
$("#" + profile_pic_field).attr('src', img);
$("#" + profile_pic_field + "_link").attr('href', note.resourcetype === "NoteUser" ?
"/accounts/user/" + note.user : note.resourcetype === "NoteClub" ?
"/accounts/club/" + note.club : "#");
}
}
}
/**
* Remove a note from the emitters.
* @param d The note to remove
* @param note_prefix The prefix of the identifiers of the <li> blocks of the emitters
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @returns an anonymous function to be compatible with jQuery events
*/
function removeNote(d, note_prefix = "note", notes_display, note_list_id, user_note_field = null, profile_pic_field = null) {
return (function () {
let new_notes_display = [];
let html = "";
notes_display.forEach(function (disp) {
if (disp.quantity > 1 || disp.id !== d.id) {
disp.quantity -= disp.id === d.id ? 1 : 0;
new_notes_display.push(disp);
html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>",
displayStyle(disp.note));
}
});
notes_display.length = 0;
new_notes_display.forEach(function (disp) {
notes_display.push(disp);
});
$("#" + note_list_id).html(html);
notes_display.forEach(function (disp) {
let obj = $("#" + note_prefix + "_" + disp.id);
obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
obj.hover(function () {
if (disp.note)
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
});
});
}
/**
* Generate an auto-complete field to query a note with its alias
* @param field_id The identifier of the text field where the alias is typed
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param notes An array containing the note objects of the buyers
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param alias_prefix The prefix of the <li> blocks for the matched aliases
* @param note_prefix The prefix of the <li> blocks for the notes of the buyers
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @param alias_click Function that is called when an alias is clicked. If this method exists and doesn't return true,
* the associated note is not displayed.
* Useful for a consumption if the item is selected before.
*/
function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_prefix = "alias",
note_prefix = "note", user_note_field = null, profile_pic_field = null, alias_click = null) {
let field = $("#" + field_id);
// Configure tooltip
field.tooltip({
html: true,
placement: 'bottom',
title: 'Loading...',
trigger: 'manual',
container: field.parent()
});
// Clear search on click
field.click(function () {
field.tooltip('hide');
field.removeClass('is-invalid');
field.val("");
});
let old_pattern = null;
// When the user type "Enter", the first alias is clicked
field.keypress(function (event) {
if (event.originalEvent.charCode === 13 && notes.length > 0) {
let li_obj = field.parent().find("ul li").first();
displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field);
li_obj.trigger("click");
}
});
// When the user type something, the matched aliases are refreshed
field.keyup(function (e) {
field.removeClass('is-invalid');
if (e.originalEvent.charCode === 13)
return;
let pattern = field.val();
// If the pattern is not modified, we don't query the API
if (pattern === old_pattern)
return;
old_pattern = pattern;
notes.length = 0;
// get matched Alias with note associated
if (pattern === "") {
field.tooltip('hide');
notes.length = 0;
return;
}
$.getJSON("/api/note/consumer/?format=json&alias="
+ pattern
+ "&search=user|club&ordering=normalized_name",
function (consumers) {
// The response arrived too late, we stop the request
if (pattern !== $("#" + field_id).val())
return;
// Build tooltip content
let aliases_matched_html = '<ul class="list-group list-group-flush">';
consumers.results.forEach(function (consumer) {
let note = consumer.note;
note.email_confirmed = consumer.email_confirmed;
if (consumer.hasOwnProperty("membership") && consumer.membership)
note.membership = consumer.membership;
else
note.membership = undefined;
let extra_css = displayStyle(note);
aliases_matched_html += li(alias_prefix + '_' + consumer.id,
consumer.name,
extra_css);
notes.push(note);
});
aliases_matched_html += '</ul>';
// Show tooltip
field.attr('data-original-title', aliases_matched_html).tooltip('show');
consumers.results.forEach(function (consumer) {
let consumer_obj = $("#" + alias_prefix + "_" + consumer.id);
consumer_obj.hover(function () {
displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field)
});
consumer_obj.click(function () {
var disp = null;
notes_display.forEach(function (d) {
// We compare the alias ids
if (d.id === consumer.id) {
d.quantity += 1;
disp = d;
}
});
// In the other case, we add a new emitter
if (disp == null) {
disp = {
name: consumer.name,
id: consumer.id,
note: consumer.note,
quantity: 1
};
notes_display.push(disp);
}
// If the function alias_click exists, it is called. If it doesn't return true, then the notes are
// note displayed. Useful for a consumption when a button is already clicked
if (alias_click && !alias_click())
return;
let note_list = $("#" + note_list_id);
let html = "";
notes_display.forEach(function (disp) {
html += li(note_prefix + "_" + disp.id,
disp.name
+ "<span class=\"badge badge-dark badge-pill\">"
+ disp.quantity + "</span>",
displayStyle(disp.note));
});
// Emitters are displayed
note_list.html(html);
// Update tooltip position
field.tooltip('update');
notes_display.forEach(function (disp) {
let line_obj = $("#" + note_prefix + "_" + disp.id);
// Hover an emitter display also the profile picture
line_obj.hover(function () {
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
// When an emitter is clicked, it is removed
line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field,
profile_pic_field));
});
})
});
});// end getJSON alias
});
}// end function autocomplete
// When a validate button is clicked, we switch the validation status
function de_validate(id, validated) {
let invalidity_reason = $("#invalidity_reason_" + id).val();
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳</strong>");
// Perform a PATCH request to the API in order to update the transaction
// If the user has insufficient rights, an error message will appear
$.ajax({
"url": "/api/note/transaction/transaction/" + id + "/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
"resourcetype": "RecurrentTransaction",
"valid": !validated,
"invalidity_reason": invalidity_reason,
},
success: function () {
// Refresh jQuery objects
$(".validate").click(de_validate);
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
},
error: function (err) {
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"];
if (!error)
error = err.responseText;
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + error, "danger");
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
}
});
}

View File

@ -0,0 +1,271 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
// When a transaction is performed, lock the interface to prevent spam clicks.
var LOCK = false;
/**
* Refresh the history table on the consumptions page.
*/
function refreshHistory() {
$("#history").load("/note/consos/ #history");
$("#most_used").load("/note/consos/ #most_used");
}
$(document).ready(function() {
// If hash of a category in the URL, then select this category
// else select the first one
if (location.hash) {
$("a[href='" + location.hash + "']").tab("show");
} else {
$("a[data-toggle='tab']").first().tab("show");
}
// When selecting a category, change URL
$(document.body).on("click", "a[data-toggle='tab']", function() {
location.hash = this.getAttribute("href");
});
// Switching in double consumptions mode should update the layout
$("#double_conso").click(function() {
$("#consos_list_div").removeClass('d-none');
$("#user_select_div").attr('class', 'col-xl-4');
$("#infos_div").attr('class', 'col-sm-5 col-xl-6');
let note_list_obj = $("#note_list");
if (buttons.length > 0 && note_list_obj.text().length > 0) {
$("#consos_list").html(note_list_obj.html());
note_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(function() {
if (LOCK)
return;
removeNote(button, "conso_button", buttons,"consos_list");
});
});
}
});
$("#single_conso").click(function() {
$("#consos_list_div").addClass('d-none');
$("#user_select_div").attr('class', 'col-xl-7');
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
let consos_list_obj = $("#consos_list");
if (buttons.length > 0) {
if (notes_display.length === 0 && consos_list_obj.text().length > 0) {
$("#note_list").html(consos_list_obj.html());
consos_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(function() {
if (LOCK)
return;
removeNote(button, "conso_button", buttons,"note_list");
});
});
}
else {
buttons.length = 0;
consos_list_obj.html("");
}
}
});
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
$("label[for='double_conso']").removeClass('active');
$("#consume_all").click(consumeAll);
});
notes = [];
notes_display = [];
buttons = [];
// When the user searches an alias, we update the auto-completion
autoCompleteNote("note", "note_list", notes, notes_display,
"alias", "note", "user_note", "profile_pic", function() {
if (buttons.length > 0 && $("#single_conso").is(":checked")) {
consumeAll();
return false;
}
return true;
});
/**
* Add a transaction from a button.
* @param dest Where the money goes
* @param amount The price of the item
* @param type The type of the transaction (content type id for RecurrentTransaction)
* @param category_id The category identifier
* @param category_name The category name
* @param template_id The identifier of the button
* @param template_name The name of the button
*/
function addConso(dest, amount, type, category_id, category_name, template_id, template_name) {
var button = null;
buttons.forEach(function(b) {
if (b.id === template_id) {
b.quantity += 1;
button = b;
}
});
if (button == null) {
button = {
id: template_id,
name: template_name,
dest: dest,
quantity: 1,
amount: amount,
type: type,
category_id: category_id,
category_name: category_name
};
buttons.push(button);
}
let dc_obj = $("#double_conso");
if (dc_obj.is(":checked") || notes_display.length === 0) {
let list = dc_obj.is(":checked") ? "consos_list" : "note_list";
let html = "";
buttons.forEach(function(button) {
html += li("conso_button_" + button.id, button.name
+ "<span class=\"badge badge-dark badge-pill\">" + button.quantity + "</span>");
});
$("#" + list).html(html);
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(function() {
if (LOCK)
return;
removeNote(button, "conso_button", buttons, list);
});
});
}
else
consumeAll();
}
/**
* Reset the page as its initial state.
*/
function reset() {
notes_display.length = 0;
notes.length = 0;
buttons.length = 0;
$("#note_list").html("");
$("#consos_list").html("");
$("#user_note").text("");
$("#profile_pic").attr("src", "/media/pic/default.png");
$("#profile_pic_link").attr("href", "#");
refreshHistory();
refreshBalance();
LOCK = false;
}
/**
* Apply all transactions: all notes in `notes` buy each item in `buttons`
*/
function consumeAll() {
if (LOCK)
return;
LOCK = true;
let error = false;
if (notes_display.length === 0) {
$("#note").addClass('is-invalid');
$("#note_list").html(li("", "<strong>Ajoutez des émetteurs.</strong>", "text-danger"));
error = true;
}
if (buttons.length === 0) {
$("#consos_list").html(li("", "<strong>Ajoutez des consommations.</strong>", "text-danger"));
error = true;
}
if (error) {
LOCK = false;
return;
}
notes_display.forEach(function(note_display) {
buttons.forEach(function(button) {
consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount,
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
});
});
}
/**
* Create a new transaction from a button through the API.
* @param source The note that paid the item (type: note)
* @param source_alias The alias used for the source (type: str)
* @param dest The note that sold the item (type: int)
* @param quantity The quantity sold (type: int)
* @param amount The price of one item, in cents (type: int)
* @param reason The transaction details (type: str)
* @param type The type of the transaction (content type id for RecurrentTransaction)
* @param category The category id of the button (type: int)
* @param template The button id (type: int)
*/
function consume(source, source_alias, dest, quantity, amount, reason, type, category, template) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": quantity,
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": type,
"resourcetype": "RecurrentTransaction",
"source": source.id,
"source_alias": source_alias,
"destination": dest,
"category": category,
"template": template
})
.done(function () {
if (!isNaN(source.balance)) {
let newBalance = source.balance - quantity * amount;
if (newBalance <= -5000)
addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " +
"succès, mais la note émettrice " + source_alias + " est en négatif sévère.",
"danger", 30000);
else if (newBalance < 0)
addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " +
"succès, mais la note émettrice " + source_alias + " est en négatif.",
"warning", 30000);
if (source.membership && source.membership.date_end < new Date().toISOString())
addMsg("Attention : la note émettrice " + source.name + " n'est plus adhérente.",
"danger", 30000);
}
reset();
}).fail(function (e) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": quantity,
"amount": amount,
"reason": reason,
"valid": false,
"invalidity_reason": "Solde insuffisant",
"polymorphic_ctype": type,
"resourcetype": "RecurrentTransaction",
"source": source,
"source_alias": source_alias,
"destination": dest,
"category": category,
"template": template
}).done(function() {
reset();
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger", 10000);
}).fail(function () {
reset();
errMsg(e.responseJSON);
});
});
}

View File

@ -0,0 +1,249 @@
/**
* jQuery Formset 1.5-pre
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
* Copyright (c) 2009, Stanislaus Madueke
* All rights reserved.
*
* Licensed under the New BSD License
* See: http://www.opensource.org/licenses/bsd-license.php
*/
;(function($) {
$.fn.formset = function(opts)
{
var options = $.extend({}, $.fn.formset.defaults, opts),
flatExtraClasses = options.extraClasses.join(' '),
totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'),
childElementSelector = 'input,select,textarea,label,div',
$$ = $(this),
applyExtraClasses = function(row, ndx) {
if (options.extraClasses) {
row.removeClass(flatExtraClasses);
row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
}
},
updateElementIndex = function(elem, prefix, ndx) {
var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
replacement = prefix + '-' + ndx + '-';
if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
},
hasChildElements = function(row) {
return row.find(childElementSelector).length > 0;
},
showAddButton = function() {
return maxForms.length == 0 || // For Django versions pre 1.2
(maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0));
},
/**
* Indicates whether delete link(s) can be displayed - when total forms > min forms
*/
showDeleteLinks = function() {
return minForms.length == 0 || // For Django versions pre 1.7
(minForms.val() == '' || (totalForms.val() - minForms.val() > 0));
},
insertDeleteLink = function(row) {
var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.');
var delButtonHTML = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>';
if (options.deleteContainerClass) {
// If we have a specific container for the remove button,
// place it as the last child of that container:
row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML);
} else if (row.is('TR')) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children('td:last').append(delButtonHTML);
} else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li>' + delButtonHTML + '</li>');
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.append(delButtonHTML);
}
// Check if we're under the minimum number of forms - not to display delete link at rendering
if (!showDeleteLinks()){
row.find('a.' + delCssSelector).hide();
}
row.find('a.' + delCssSelector).click(function() {
var row = $(this).parents('.' + options.formCssClass),
del = row.find('input:hidden[id $= "-DELETE"]'),
buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'),
forms;
if (del.length) {
// We're dealing with an inline formset.
// Rather than remove this form from the DOM, we'll mark it as deleted
// and hide it, then let Django handle the deleting:
del.val('on');
row.hide();
forms = $('.' + options.formCssClass).not(':hidden');
} else {
row.remove();
// Update the TOTAL_FORMS count:
forms = $('.' + options.formCssClass).not('.formset-custom-template');
totalForms.val(forms.length);
}
for (var i=0, formCount=forms.length; i<formCount; i++) {
// Apply `extraClasses` to form rows so they're nicely alternating:
applyExtraClasses(forms.eq(i), i);
if (!del.length) {
// Also update names and IDs for all child controls (if this isn't
// a delete-able inline formset) so they remain in sequence:
forms.eq(i).find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, i);
});
}
}
// Check if we've reached the minimum number of forms - hide all delete link(s)
if (!showDeleteLinks()){
$('a.' + delCssSelector).each(function(){$(this).hide();});
}
// Check if we need to show the add button:
if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show();
// If a post-delete callback was provided, call it with the deleted form:
if (options.removed) options.removed(row);
return false;
});
};
$$.each(function(i) {
var row = $(this),
del = row.find('input:checkbox[id $= "-DELETE"]');
if (del.length) {
// If you specify "can_delete = True" when creating an inline formset,
// Django adds a checkbox to each form in the formset.
// Replace the default checkbox with a hidden field:
if (del.is(':checked')) {
// If an inline formset containing deleted forms fails validation, make sure
// we keep the forms hidden (thanks for the bug report and suggested fix Mike)
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" value="on" />');
row.hide();
} else {
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
}
// Hide any labels associated with the DELETE checkbox:
$('label[for="' + del.attr('id') + '"]').hide();
del.remove();
}
if (hasChildElements(row)) {
row.addClass(options.formCssClass);
if (row.is(':visible')) {
insertDeleteLink(row);
applyExtraClasses(row, i);
}
}
});
if ($$.length) {
var hideAddButton = !showAddButton(),
addButton, template;
if (options.formTemplate) {
// If a form template was specified, we'll clone it to generate new form instances:
template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
template.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, '__prefix__');
});
insertDeleteLink(template);
} else {
// Otherwise, use the last form in the formset; this works much better if you've got
// extra (>= 1) forms (thnaks to justhamade for pointing this out):
if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide();
template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
template.find('input:hidden[id $= "-DELETE"]').remove();
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
template.find(childElementSelector).not(options.keepFieldValues).each(function() {
var elem = $(this);
// If this is a checkbox or radiobutton, uncheck it.
// This fixes Issue 1, reported by Wilson.Andrew.J:
if (elem.is('input:checkbox') || elem.is('input:radio')) {
elem.attr('checked', false);
} else {
elem.val('');
}
});
}
// FIXME: Perhaps using $.data would be a better idea?
options.formTemplate = template;
var addButtonHTML = '<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>';
if (options.addContainerClass) {
// If we have a specific container for the "add" button,
// place it as the last child of that container:
var addContainer = $('[class*="' + options.addContainerClass + '"');
addContainer.append(addButtonHTML);
addButton = addContainer.find('[class="' + options.addCssClass + '"]');
} else if ($$.is('TR')) {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
buttonRow = $('<tr><td colspan="' + numCols + '">' + addButtonHTML + '</tr>').addClass(options.formCssClass + '-add');
$$.parent().append(buttonRow);
addButton = buttonRow.find('a');
} else {
// Otherwise, insert it immediately after the last form:
$$.filter(':last').after(addButtonHTML);
addButton = $$.filter(':last').next();
}
if (hideAddButton) addButton.hide();
addButton.click(function() {
var formCount = parseInt(totalForms.val()),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this),
delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.');
applyExtraClasses(row, formCount);
row.insertBefore(buttonRow).show();
row.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, formCount);
});
totalForms.val(formCount + 1);
// Check if we're above the minimum allowed number of forms -> show all delete link(s)
if (showDeleteLinks()){
$('a.' + delCssSelector).each(function(){$(this).show();});
}
// Check if we've exceeded the maximum allowed number of forms:
if (!showAddButton()) buttonRow.hide();
// If a post-add callback was supplied, call it with the added form:
if (options.added) options.added(row);
return false;
});
}
return $$;
};
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: 'form', // The form prefix for your django formset
formTemplate: null, // The jQuery selection cloned to generate new form instances
addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link
addContainerClass: null, // Container CSS class for the add link
deleteContainerClass: null, // Container CSS class for the delete link
addCssClass: 'add-row', // CSS class applied to the add link
deleteCssClass: 'delete-row', // CSS class applied to the delete link
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
added: null, // Function called each time a new form is added
removed: null, // Function called each time a form is deleted
hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button)
};
})(jQuery);

View File

@ -0,0 +1,417 @@
var LOCK = false;
sources = [];
sources_notes_display = [];
dests = [];
dests_notes_display = [];
function refreshHistory() {
$("#history").load("/note/transfer/ #history");
}
function reset(refresh=true) {
sources_notes_display.length = 0;
sources.length = 0;
dests_notes_display.length = 0;
dests.length = 0;
$("#source_note_list").html("");
$("#dest_note_list").html("");
let source_field = $("#source_note");
source_field.val("");
let event = jQuery.Event("keyup");
event.originalEvent = {charCode: 97};
source_field.trigger(event);
source_field.removeClass('is-invalid');
let dest_field = $("#dest_note");
dest_field.val("");
dest_field.trigger(event);
dest_field.removeClass('is-invalid');
let amount_field = $("#amount");
amount_field.val("");
amount_field.removeClass('is-invalid');
$("#amount-required").html("");
let reason_field = $("#reason");
reason_field.val("");
reason_field.removeClass('is-invalid');
$("#reason-required").html("");
$("#last_name").val("");
$("#first_name").val("");
$("#bank").val("");
$("#user_note").val("");
$("#profile_pic").attr("src", "/media/pic/default.png");
$("#profile_pic_link").attr("href", "#");
if (refresh) {
refreshBalance();
refreshHistory();
}
LOCK = false;
}
$(document).ready(function() {
/**
* If we are in credit/debit mode, check that only one note is entered.
* More over, get first name and last name to autocomplete fields.
*/
function checkUniqueNote() {
if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) {
let arr = $("#type_credit").is(":checked") ? dests_notes_display : sources_notes_display;
if (arr.length === 0)
return;
let last = arr[arr.length - 1];
arr.length = 0;
arr.push(last);
last.quantity = 1;
if (!last.note.user) {
$.getJSON("/api/note/note/" + last.note.id + "/?format=json", function(note) {
last.note.user = note.user;
$.getJSON("/api/user/" + last.note.user + "/", function(user) {
$("#last_name").val(user.last_name);
$("#first_name").val(user.first_name);
});
});
}
else {
$.getJSON("/api/user/" + last.note.user + "/", function(user) {
$("#last_name").val(user.last_name);
$("#first_name").val(user.first_name);
});
}
}
return true;
}
autoCompleteNote("source_note", "source_note_list", sources, sources_notes_display,
"source_alias", "source_note", "user_note", "profile_pic", checkUniqueNote);
autoCompleteNote("dest_note", "dest_note_list", dests, dests_notes_display,
"dest_alias", "dest_note", "user_note", "profile_pic", checkUniqueNote);
let source = $("#source_note");
let dest = $("#dest_note");
$("#type_transfer").click(function() {
if (LOCK)
return;
$("#source_me_div").removeClass('d-none');
$("#source_note").removeClass('is-invalid');
$("#dest_note").removeClass('is-invalid');
$("#special_transaction_div").addClass('d-none');
source.attr('disabled', false);
$("#source_note_list").removeClass('d-none');
dest.attr('disabled', false);
$("#dest_note_list").removeClass('d-none');
});
$("#type_credit").click(function() {
if (LOCK)
return;
$("#source_me_div").addClass('d-none');
$("#source_note").removeClass('is-invalid');
$("#dest_note").removeClass('is-invalid');
$("#special_transaction_div").removeClass('d-none');
$("#source_note_list").addClass('d-none');
$("#dest_note_list").removeClass('d-none');
source.attr('disabled', true);
source.val($("#credit_type option:selected").text());
source.tooltip('hide');
dest.attr('disabled', false);
dest.val('');
dest.tooltip('hide');
if (dests_notes_display.length > 1) {
$("#dest_note_list").html('');
dests_notes_display.length = 0;
}
});
$("#type_debit").click(function() {
if (LOCK)
return;
$("#source_me_div").addClass('d-none');
$("#source_note").removeClass('is-invalid');
$("#dest_note").removeClass('is-invalid');
$("#special_transaction_div").removeClass('d-none');
$("#source_note_list").removeClass('d-none');
$("#dest_note_list").addClass('d-none');
source.attr('disabled', false);
source.val('');
source.tooltip('hide');
dest.attr('disabled', true);
dest.val($("#credit_type option:selected").text());
dest.tooltip('hide');
if (sources_notes_display.length > 1) {
$("#source_note_list").html('');
sources_notes_display.length = 0;
}
});
$("#credit_type").change(function() {
let type = $("#credit_type option:selected").text();
if ($("#type_credit").is(":checked"))
source.val(type);
else
dest.val(type);
});
// Ensure we begin in transfer mode. Removing these lines may cause problems when reloading.
let type_transfer = $("#type_transfer"); // Default mode
type_transfer.removeAttr('checked');
$("#type_credit").removeAttr('checked');
$("#type_debit").removeAttr('checked');
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
if (location.hash)
$("#type_" + location.hash.substr(1)).click();
else
type_transfer.click();
location.hash = "";
$("#source_me").click(function() {
if (LOCK)
return;
// Shortcut to set the current user as the only emitter
sources_notes_display.length = 0;
sources.length = 0;
$("#source_note_list").html("");
let source_note = $("#source_note");
source_note.focus();
source_note.val("");
let event = jQuery.Event("keyup");
event.originalEvent = {charCode: 97};
source_note.trigger(event);
source_note.val(username);
event = jQuery.Event("keyup");
event.originalEvent = {charCode: 97};
source_note.trigger(event);
let fill_note = function() {
if (sources.length === 0) {
setTimeout(fill_note, 100);
return;
}
event = jQuery.Event("keypress");
event.originalEvent = {charCode: 13};
source_note.trigger(event);
source_note.tooltip('hide');
source_note.val('');
$("#dest_note").focus();
};
fill_note();
});
});
$("#btn_transfer").click(function() {
if (LOCK)
return;
LOCK = true;
let error = false;
let amount_field = $("#amount");
amount_field.removeClass('is-invalid');
$("#amount-required").html("");
let reason_field = $("#reason");
reason_field.removeClass('is-invalid');
$("#reason-required").html("");
if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) {
amount_field.addClass('is-invalid');
$("#amount-required").html("<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>");
error = true;
}
let amount = Math.floor(100 * amount_field.val());
if (amount > 2147483647) {
amount_field.addClass('is-invalid');
$("#amount-required").html("<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>");
error = true;
}
if (!reason_field.val()) {
reason_field.addClass('is-invalid');
$("#reason-required").html("<strong>Ce champ est requis.</strong>");
error = true;
}
if (!sources_notes_display.length && !$("#type_credit").is(':checked')) {
$("#source_note").addClass('is-invalid');
error = true;
}
if (!dests_notes_display.length && !$("#type_debit").is(':checked')) {
$("#dest_note").addClass('is-invalid');
error = true;
}
if (error) {
LOCK = false;
return;
}
let reason = reason_field.val();
if ($("#type_transfer").is(':checked')) {
// We copy the arrays to ensure that transactions are well-processed even if the form is reset
[...sources_notes_display].forEach(function (source) {
[...dests_notes_display].forEach(function (dest) {
if (source.note.id === dest.note.id) {
addMsg("Attention : la transaction de " + pretty_money(amount) + " de la note " + source.name
+ " vers la note " + dest.name + " n'a pas été faite car il s'agit de la même note au départ" +
" et à l'arrivée.","warning", 10000);
return;
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity,
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": source.note.id,
"source_alias": source.name,
"destination": dest.note.id,
"destination_alias": dest.name
}).done(function () {
if (source.note.membership && source.note.membership.date_end < new Date().toISOString())
addMsg("Attention : la note émettrice " + source.name + " n'est plus adhérente.",
"danger", 30000);
if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString())
addMsg("Attention : la note destination " + dest.name + " n'est plus adhérente.",
"danger", 30000);
if (!isNaN(source.note.balance)) {
let newBalance = source.note.balance - source.quantity * dest.quantity * amount;
if (newBalance <= -5000) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note "
+ source.name + " vers la note " + dest.name + " a été fait avec succès, " +
"mais la note émettrice est en négatif sévère.", "danger", 10000);
reset();
return;
}
else if (newBalance < 0) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note "
+ source.name + " vers la note " + dest.name + " a été fait avec succès, " +
"mais la note émettrice est en négatif.", "warning", 10000);
reset();
return;
}
}
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a été fait avec succès !", "success", 10000);
reset();
}).fail(function (err) { // do it again but valid = false
let errObj = JSON.parse(err.responseText);
if (errObj["non_field_errors"]) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + errObj["non_field_errors"], "danger");
return;
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity,
"amount": amount,
"reason": reason,
"valid": false,
"invalidity_reason": "Solde insuffisant",
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": source.note.id,
"source_alias": source.name,
"destination": dest.note.id,
"destination_alias": dest.name
}).done(function () {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000);
reset();
}).fail(function (err) {
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]
if (!error)
error = err.responseText;
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + error, "danger");
});
});
});
});
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val();
let user_note;
let alias;
let given_reason = reason;
let source_id, dest_id;
if ($("#type_credit").is(':checked')) {
user_note = dests_notes_display[0].note;
alias = dests_notes_display[0].name;
source_id = special_note;
dest_id = user_note.id;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
else {
user_note = sources_notes_display[0].note;
alias = sources_notes_display[0].name;
source_id = user_note.id;
dest_id = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1,
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "SpecialTransaction",
"source": source_id,
"source_alias": sources_notes_display.length ? alias : null,
"destination": dest_id,
"destination_alias": dests_notes_display.length ? alias : null,
"last_name": $("#last_name").val(),
"first_name": $("#first_name").val(),
"bank": $("#bank").val()
}).done(function () {
addMsg("Le crédit/retrait a bien été effectué !", "success", 10000);
if (user_note.membership && user_note.membership.date_end < new Date().toISOString())
addMsg("Attention : la note " + alias + " n'est plus adhérente.", "danger", 10000);
reset();
}).fail(function (err) {
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]
if (!error)
error = err.responseText;
addMsg("Le crédit/retrait a échoué : " + error, "danger", 10000);
});
}
});