Better k-fet js and more

JavaScript
----------
- Basic classes that can be inherited to define a new class for a
  django model in javascript.
- Formatters classes are used to render properties and attributes of
  the instances of models classes.
- New classes to handle Account, Checkout, Statement models.
- Refactor K-Psul JS (part n/m).
- Better file organization.

Views
-----
- 'kpsul.checkout_data' is cleaner. Last statement is added to the JSON
  response with GET paramater 'last_statement'.
- 'account.read.json' is merged in account.read. JSON response is sent if
  GET parametter 'format' is set to 'json'.
- Fix PEP8 of concerned views.

New requirement: django-js-reverse
----------------------------------
Used to resolve the URLs defined in the project in JavaScript.
See https://github.com/ierror/django-js-reverse
This commit is contained in:
Aurélien Delobelle 2017-02-23 22:07:38 +01:00
parent ee848d210e
commit a9cb50b38d
10 changed files with 1168 additions and 574 deletions

View file

@ -50,6 +50,7 @@ INSTALLED_APPS = (
'kfet', 'kfet',
'channels', 'channels',
'widget_tweaks', 'widget_tweaks',
'django_js_reverse',
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (

View file

@ -14,9 +14,11 @@ from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.views.decorators.cache import cache_page
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.contrib.auth import views as django_views from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views from django_cas_ng import views as django_cas_views
from django_js_reverse.views import urls_js
from gestioncof import views as gestioncof_views, csv_views from gestioncof import views as gestioncof_views, csv_views
from gestioncof.urls import export_patterns, petitcours_patterns, \ from gestioncof.urls import export_patterns, petitcours_patterns, \
@ -85,6 +87,7 @@ urlpatterns = [
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof), url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^k-fet/', include('kfet.urls')), url(r'^k-fet/', include('kfet.urls')),
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
] ]
if settings.DEBUG: if settings.DEBUG:

View file

@ -38,10 +38,10 @@ input[type=number]::-webkit-outer-spin-button {
height:120px; height:120px;
} }
#account[data-balance="ok"] #account_form input { background:#009011; color:#FFF;} #account[data_balance="ok"] #account_form input { background:#009011; color:#FFF;}
#account[data-balance="low"] #account_form input { background:#EC6400; color:#FFF; } #account[data_balance="low"] #account_form input { background:#EC6400; color:#FFF; }
#account[data-balance="neg"] #account_form input { background:#C8102E; color:#FFF; } #account[data_balance="neg"] #account_form input { background:#C8102E; color:#FFF; }
#account[data-balance="frozen"] #account_form input { background:#000FBA; color:#FFF; } #account[data_balance="frozen"] #account_form input { background:#000FBA; color:#FFF; }
#account_form { #account_form {
padding:0; padding:0;
@ -79,7 +79,7 @@ input[type=number]::-webkit-outer-spin-button {
font-size:12px; font-size:12px;
} }
#account_data #account-balance { #account_data #account-balance_ukf {
height:40px; height:40px;
line-height:40px; line-height:40px;
@ -112,7 +112,7 @@ input[type=number]::-webkit-outer-spin-button {
font-size:14px; font-size:14px;
line-height:24px; line-height:24px;
} }
#account_data #account-balance { #account_data #account-balance_ukf {
font-size:50px; font-size:50px;
line-height:60px; line-height:60px;
height:60px; height:60px;

View file

@ -0,0 +1,685 @@
/**
* @file Interact with k-fet API.
* @copyright 2017 cof-geek
* @license MIT
*/
/**
* Get and store K-Psul config from API.
* <br><br>
*
* Config should be accessed statically only.
*/
class Config {
/**
* Get or create config object.
* @private
* @return {object} object - config keys/values
*/
static _get_or_create_config() {
if (window.config === undefined)
window.config = {};
return window.config;
}
/**
* Get config from API.
* @param {jQueryAjaxComplete} [callback] - A function to be called when
* the request finishes.
*/
static reset(callback) {
$.getJSON(Urls['kfet.kpsul.get_settings']())
.done(function(data) {
for (var key in data) {
Config.set(key, data[key]);
}
})
.always(callback);
}
/**
* Get value for key in config.
* @param {string} key
*/
static get(key) {
return this._get_or_create_config()[key];
}
/**
* Set value for key in config.
* @param {string} key
* @param {*} value
*/
static set(key, value) {
// API currently returns string for Decimal type
if (['addcost_amount', 'subvention_cof'].indexOf(key) > -1)
value = floatCheck(value);
this._get_or_create_config()[key] = value;
}
}
/* ---------- ---------- */
/**
* Virtual namespace for models.
* <br><br>
*
* A model subclasses {@link Models.ModelObject}.<br>
* A model whose instances can be got from API subclasses
* {@link Models.APIModelObject}.<br>
* These two classes should not be used directly.
* <br><br>
*
* Models with API support:
* {@link Models.Account} (partial),
* {@link Models.Checkout} (partial).
* <br>
* Models without API support:
* {@link Models.Statement}.
*
* @namespace Models
*/
/**
* Extended object
* @memberof Models
*/
class ModelObject {
/**
* These properties always exist on instances of this class.
* @abstract
* @type {string[]}
*/
static get props() { return []; }
/**
* Default values for properties given by <tt>props</tt> static member.
* @abstract
* @type {Object.<string, *>}
*/
static get default_data() { return {}; }
/**
* Create new instance from data or default values.
* @param {Object} [data={}] - data to store in instance
*/
constructor(data) {
this.from(data || {});
}
/**
* Check if an instance is empty.
* @return {boolean}
*/
is_empty() {
return ( this.id === undefined ) ? false : this.id == 0;
}
/**
* Returns a {@link Formatters|Formatter} used for rendering.
* @default {@link Formatters.Formatter}
* @type {Formatter}
*/
formatter() { return Formatter; }
/**
* Set properties of this instance from data object ones. If a property is
* not present in data object, its value fallback to
* {@link Models.ModelObject.default_data} one.
* @param {Object} data
* @todo Restrict to props
*/
from(data) {
// TODO: add restrict
$.extend(this, this.constructor.default_data, data);
}
/**
* Clear properties to {@link Models.ModelObject.default_data|default_data}.
*/
clear() {
this.from({});
}
/**
* Display stored data in container.
* @param {jQuery} $container
* @param {Object} [options] Options for formatter render method.
* @param {Formatters.Formatter}
* [formatter={@link Models.ModelObject#formatter|this.formatter()}]
* Formatter class to use.
*/
display($container, options, formatter) {
formatter = formatter || this.formatter();
formatter.render(this, $container, options);
}
}
/**
* Describes a model whose instances can be obtained through API.
* @extends Models.ModelObject
* @memberof Models
*/
class APIModelObject extends ModelObject {
/**
* Request url to get array of model instances data.
* @abstract
* @type {string}
*/
static get url_model() {}
/**
* Request url to get a single model instance data.
* @abstract
* @param {*} api_pk - Identifier of a model instance in api requests.
* @return {string}
*/
static url_object_for(api_pk) {}
/**
* See {@link Models.ModelObject|new ModelObject(data)}.
* @param {Object} [data={}] - data to store in instance
*/
constructor(data) {
super(data);
if (this.id === undefined)
this.id = 0;
}
/**
* Identifier of the model instance in api requests.
* @default <tt>this.id</tt>
*/
get api_pk() { return this.id; }
/**
* Request url used to get current instance data.
*/
get url_object() {
if (this._url_object === undefined)
return this.is_empty() ? '' : this.constructor.url_object_for(this.api_pk);
return this._url_object;
}
set url_object(v) { this._url_object = v; }
/**
* Get data of a distant model instance. It sends a GET HTTP request to
* {@link Models.APIModelObject#url_object}.
* @param {object} [api_options] Additional data appended to the request.
* @param {jQueryAjaxSuccess} [on_success] A function to be called if the request succeeds.
* @param {jQueryAjaxError} [on_error] A function to be called if the request fails.
*/
fromAPI(api_options, on_success, on_error) {
var that = this;
api_options = api_options || {};
on_success = on_success || $.noop;
on_error = on_error || $.noop;
api_options['format'] = 'json';
$.getJSON(this.url_object, api_options)
.done(function (json, textStatus, jqXHR) {
that.from(json)
on_success(json, textStatus, jqXHR);
})
.fail(on_error);
}
/**
* Get data of a distant model instance via its id.
* @param {*} api_pk
* @param {object} [api_options]
* @param {jQueryAjaxSuccess} [on_success]
* @param {jQueryAjaxError} [on_error]
* @see {@link Models.APIModelObject#fromAPI|fromAPI}
*/
get_by_apipk(api_pk, api_options, on_success, on_error) {
this.url_object = this.constructor.url_object_for(api_pk);
this.fromAPI(api_options, on_success, on_error);
}
/**
* Add <tt>empty_props</tt> and <tt>empty_attrs</tt> options to
* <tt>render</tt> call in {@link Models.ModelObject#display} if instance
* is empty.
* @see {@link Models.ModelObject#display}
* @see {@link Models.APIModelObject#is_empty}
*/
display($container, options, formatter) {
if (this.is_empty()) {
options.empty_props = true;
options.empty_attrs = true;
}
return ModelObject.prototype.display.call(this, $container, options, formatter);
}
}
/**
* Account model. Can be accessed through API.
* @extends Models.APIModelObject
* @memberof Models
*/
class Account extends APIModelObject {
/**
* Properties retrieved through API.
* @default <tt>['id', 'trigramme', 'name', 'nickname', 'email', 'is_cof',
* 'promo', 'balance', 'is_frozen', 'departement']</tt>
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
return ['id', 'trigramme', 'name', 'nickname', 'email', 'is_cof',
'promo', 'balance', 'is_frozen', 'departement'];
}
/**
* Default values for Account model instances.
* @default <tt>{ 'id': 0, 'trigramme': '', 'name': '', 'nickname': '',
* 'email': '', ''is_cof': false, 'promo': '', 'balance': 0,
* 'is_frozen': false, 'departement': '' }</tt>
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
*/
static get default_data() {
return {
'id': 0, 'trigramme': '', 'name': '', 'nickname': '', 'email': '',
'is_cof' : false, 'promo': '', 'balance': 0, 'is_frozen': false,
'departement': '',
};
};
/**
* @default <tt>django-js-reverse('kfet.account')</tt>
* @see {@link Models.APIModelObject.url_model|APIModelObject.url_model}
*/
static get url_model() { return Urls['kfet.account'](); }
/**
* @default <tt>django-js-reverse('kfet.account.read')(trigramme)</tt>
* @param {string} trigramme
* @see {@link Models.APIModelObject.url_object_for|APIModelObject.url_object_for}
*/
static url_object_for(trigramme) {
var trigramme_url = encodeURIComponent(trigramme);
return Urls['kfet.account.read'](trigramme_url);
}
/**
* @default <tt>this.trigramme</tt>
*/
get api_pk() { return this.trigramme; }
/**
* @default {@link Formatters.AccountFormatter}
*/
formatter() {
return (this.trigramme == 'LIQ') ? LIQFormatter : AccountFormatter;
}
// take care of "balance" type
// API currently returns a string object (serialization of Decimal type within Django)
get balance() { return this._balance; }
set balance(v) { return this._balance = floatCheck(v); }
/**
* Balance converted to UKF according to cof status.
*/
get balance_ukf() { return amountToUKF(this.balance, this.is_cof); }
}
/**
* Checkout model. Can be accessed through API.
* @extends Models.APIModelObject
* @memberof Models
*/
class Checkout extends APIModelObject {
/**
* Properties retrieved through API.
* @default <tt>['id', 'name', 'balance', 'valid_from', 'valid_to']</tt>
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
return ['id', 'name', 'balance', 'valid_from', 'valid_to'];
}
/**
* Default values for Account model instances.
* @default <tt>{ 'id': 0, 'name': '', 'balance': 0, 'valid_from': '',
* 'valid_to': '' }</tt>
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
*/
static get default_data() {
return {
'id': 0, 'name': '', 'balance': 0, 'valid_from': '', 'valid_to': '',
};
}
/**
* Not supported by API yet.
* @see {@link Models.APIModelObject.url_model|APIModelObject.url_model}
*/
static get url_model() { return ''; }
/**
* @default <tt>django-js-reverse('kfet.kpsul.checkout_data.read')(pk)</tt>
* @param {string} api_pk - a checkout id
* @see {@link Models.APIModelObject.url_object_for|APIModelObject.url_object_for}
*/
static url_object_for(api_pk) {
return Urls['kfet.kpsul.checkout_data.read'](api_pk);
}
/**
* @default {@link Formatters.CheckoutFormatter}
*/
formatter() {
return CheckoutFormatter;
}
// take care of "balance" type
// API currently returns a string object (serialization of Decimal type within Django)
get balance() { return this._balance; }
set balance(v) { this._balance = floatCheck(v); }
}
/**
* Statement model. Cannot be accessed through API.
* @extends Models.ModelObject
* @memberof Models
*/
class Statement extends ModelObject {
/**
* Properties associated to a statement.
* @default <tt>['id', 'at', 'balance_old', 'balance_new', 'by']</tt>
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
return ['id', 'at', 'balance_old', 'balance_new', 'by'];
}
/**
* Default values for Statement model instances.
* @default <tt>{ 'id': 0, 'at': '', 'balance_old': 0, 'balance_new': 0,
* 'by': '' }</tt>
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
*/
static get default_data() {
return {
'id': 0, 'at': '', 'balance_old': 0, 'balance_new': 0, 'by': '',
};
}
/**
* @default {@link Formatters.StatementFormatter}
*/
formatter() {
return StatementFormatter;
}
// take care of "balance" type
// API currently returns a string object (serialization of Decimal type within Django)
get balance_old() { return this._balance_old; }
set balance_old(v) { this._balance_old = floatCheck(v); }
get balance_new() { return this._balance_new; }
set balance_new(v) { this._balance_new = floatCheck(v); }
get at() { return this._at; }
set at(v) { this._at = moment.isMoment(v) ? v : moment.tz(v, 'UTC'); }
}
/* ---------- ---------- */
/**
* Virtual namespace for formatters.
* <br><br>
*
* A formatter subclasses {@link Formatters.Formatter}.<br>
* Formatters should be accessed statically only.
*
* @namespace Formatters
*/
/**
* @memberof Formatters
*/
class Formatter {
static get props() { return []; }
static get attrs() { return []; }
/**
* Get value of a property through current formatter if defined, object itself otherwise.
*
* @param {*} object - Object used for formatting
* @param {string} name - Name of property to render
* @param {string} [prefix=''] - Prefix used for formatter method search
* @return {*} Formatted property value
*/
static get(object, name, prefix) {
prefix = prefix || '';
var method = prefix + name;
if (this[method] !== undefined)
return this[method](object);
return object[name];
}
/**
* Get formatted value of a property using prefix 'prop_'.
* @see Formatter.get
*/
static get_prop(object, name) {
return this.get(object, name, 'prop_');
}
/**
* Get formatted value of a property using prefix 'attr_'.
* @see Formatter.get
*/
static get_attr(obj, attr) {
return this.get(obj, attr, 'attr_');
}
/**
* Render container.
* @param {*} object - Object used for formatting
* @param {jQueryNode} $container
* @param {object} [options]
* <tt>options['props']</tt> should be an array of
* properties (default: <tt>class.props</tt>).<br>
* <tt>options['prefix_prop']</tt> is added in front of
* properties names in selector (default: <tt>'.'</tt>).<br>
* <tt>options['prefix_attr']</tt> is added in front of
* attributes names for container (default: <tt>''</tt>).
*/
static render(object, $container, options) {
options.props = options.props || [];
options.attrs = options.attrs || [];
var props = options.override_props ? options.props : this.props.concat(options.props);
var attrs = options.override_attrs ? options.attrs : this.attrs.concat(options.attrs);
var prefix_prop = options.prefix_prop !== undefined ? options.prefix_prop : '.';
var prefix_attr = options.prefix_attr !== undefined ? options.prefix_attr : '';
for (var i in props) {
var selector = prefix_prop + props[i];
var html = options.empty_props ? '' : this.get_prop(object, props[i]);
$container.find( selector ).html( html );
}
for (var i in attrs) {
var name = prefix_attr + attrs[i];
var value = options.empty_attrs ? '' : this.get_attr(object, attrs[i]);
$container.attr( name, value );
}
return $container;
}
}
/**
* @memberof Formatters
* @extends Formatters.Formatter
*/
class AccountFormatter extends Formatter {
/**
* Properties renderable in html.
* @default {@link Models.Account.props}<tt> + ['balance_ukf']</tt>
*/
static get props() {
return Account.props.concat(['balance_ukf']);
}
/**
* Added attributes to $container element.
* @default <tt>['data-balance']</tt>
*/
static get attrs() {
return ['data_balance'];
}
static get _data_balance() {
return {
'default': '', 'frozen': 'frozen',
'ok': 'ok', 'low': 'low', 'neg': 'neg',
};
}
/**
* <tt>a.balance</tt> with two decimals.
*/
static prop_balance(a) {
return a.balance.toFixed(2);
}
/**
* <tt>'COF' if account is cof or 'Non-COF'
*/
static prop_is_cof(a) {
return a.is_cof ? 'COF' : 'Non-COF';
}
/**
* Value of data_attribute according to is_frozen status and balance of
* account <tt>a</tt>.
*/
static attr_data_balance(a) {
if (a.is_frozen) { return this._data_balance.frozen; }
else if (a.balance >= 5) { return this._data_balance.ok; }
else if (a.balance >= 0) { return this._data_balance.low; }
else /* a.balance < 0 */ { return this._data_balance.neg; }
}
}
/**
* @memberof Formatters
* @extends Formatters.AccountFormatter
*/
class LIQFormatter extends AccountFormatter {
/**
* Rendering a property always returns the empty string
* @default <tt>''</tt>
*/
static get_prop() {
return '';
}
/**
* Attribute <tt>data_balance</tt> is always <tt>ok</tt>.
*/
static attr_data_balance(a) { return this._data_balance.ok; }
}
/**
* @memberof Formatters
* @extends Formatters.Formatter
*/
class CheckoutFormatter extends Formatter {
/**
* Properties renderable to html.
* @default {@link Models.Checkout.props}
*/
static get props() {
return Checkout.props;
}
/**
* <tt>c.balance</tt> with two decimals.
*/
static prop_balance(c) {
return c.balance.toFixed(2);
}
}
/**
* @memberof Formatters
* @extends Formatters.Formatter
*/
class StatementFormatter extends Formatter {
/**
* Properties renderable to html.
* @default {@link Models.Statement.props}
*/
static get props() {
return Statement.props;
}
/**
* <tt>s.balance_old</tt> with two decimals.
*/
static prop_balance_old(s) {
return s.balance_old.toFixed(2);
}
/**
* <tt>s.balance_new</tt> with two decimals.
*/
static prop_balance_new(s) {
return s.balance_new.toFixed(2);
}
/**
* <tt>s.at</tt> formatted as <tt>DD/MM/YY à HH:MM</tt>.
*/
static prop_at(s) {
return moment.isMoment(s.at) ? s.at.tz('Europe/Paris').format('DD/MM/YY à HH:mm') : s.at;
}
}

View file

@ -1,3 +1,30 @@
/**
* @file Miscellaneous JS definitions for <tt>k-fet</tt> app.
* @copyright 2017 cof-geek
* @license MIT
*/
/**
* String method
* @memberof String
* @return {String} String formatted as trigramme
*/
String.prototype.formatTri = function() {
return this.toUpperCase().substr(0, 3);
}
/**
* String method
* @global
* @return {Boolean} true iff String follows trigramme pattern
*/
String.prototype.isValidTri = function() {
var pattern = /^[^a-z]{3}$/;
return pattern.test(this);
}
$(document).ready(function() { $(document).ready(function() {
$(window).scroll(function() { $(window).scroll(function() {
if ($(window).width() >= 768 && $(this).scrollTop() > 72.6) { if ($(window).width() >= 768 && $(this).scrollTop() > 72.6) {
@ -38,7 +65,7 @@ function amountDisplay(amount, is_cof=false, tri='') {
} }
function amountToUKF(amount, is_cof=false) { function amountToUKF(amount, is_cof=false) {
var coef_cof = is_cof ? 1 + Config.getAll().subvention_cof / 100 : 1; var coef_cof = is_cof ? 1 + Config.get('subvention_cof') / 100 : 1;
return Math.round(amount * coef_cof * 10); return Math.round(amount * coef_cof * 10);
} }

View file

@ -0,0 +1,369 @@
class KPsulManager {
constructor(env) {
this._env = env;
this.account_manager = new AccountManager(this);
this.checkout_manager = new CheckoutManager(this);
}
reset(soft) {
soft = soft || false;
this.account_manager.reset();
if (!soft) {
this.checkout_manager.reset();
}
}
}
class AccountManager {
constructor() {
this._$container = $('#account');
this.account = new Account();
this.selection = new AccountSelection(this);
this.search = new AccountSearch(this);
}
is_empty() { return this.account.is_empty(); }
display() {
this._display_data();
this._display_buttons();
}
_display_data() {
this.account.display(this._$container, {
'prefix_prop': '#account-',
});
}
_display_buttons() {
// dirty
var buttons = '';
if (this.is_empty()) {
var trigramme = this.selection.get();
if (trigramme.isValidTri()) {
var url_base = "{% url 'kfet.account.create' %}";
var url = url_base + '?trigramme=' + encodeURIComponent(trigramme);
buttons += '<a href="'+url+'" class="btn btn-primary" target="_blank" title="Créer ce compte"><span class="glyphicon glyphicon-plus"></span></a>';
} else { /* trigramme input is empty or invalid */
buttons += '<button class="btn btn-primary search" title="Rechercher"><span class="glyphicon glyphicon-search"></span></button>';
}
} else { /* an account is loaded */
var url = Urls['kfet.account.update'](encodeURIComponent(this.account.trigramme));
buttons += '<a href="'+url+'" class="btn btn-primary" target="_blank" title="Modifier ce compte"><span class="glyphicon glyphicon-cog"></span></a>';
}
this._$container.find('.buttons').html(buttons);
}
update(trigramme) {
if (trigramme !== undefined)
this.selection.set(trigramme);
trigramme = trigramme || this.selection.get();
if (trigramme.isValidTri()) {
this.account.get_by_apipk(trigramme, {},
() => this._update_on_success(),
() => this.reset_data()
);
} else {
this.reset_data();
}
}
_update_on_success() {
$('#id_on_acc').val(this.account.id);
this.display();
kpsul._env.articleSelect.focus();
kpsul._env.updateBasketAmount();
kpsul._env.updateBasketRel();
}
reset() {
$('#id_on_acc').val(0);
this.selection.reset();
this.search.reset();
this.reset_data();
}
reset_data() {
this.account.clear();
this.display();
}
focus() {
this.selection.focus();
return this;
}
}
class AccountSelection {
constructor(manager) {
this.manager = manager;
this._$input = $('#id_trigramme');
this._init_events();
}
_init_events() {
var that = this;
// user change trigramme
this._$input
.on('input', () => this.manager.update());
// LIQ shortcuts
this._$input
.on('keydown', function(e) {
// keys: 13:Enter|40:Arrow-Down
if (e.keyCode == 13 || e.keyCode == 40)
that.manager.update('LIQ');
});
}
get() {
return this._$input.val().formatTri();
}
set(v) {
this._$input.val(v);
}
focus() {
this._$input.focus();
}
reset() {
this.set('');
}
}
class AccountSearch {
constructor(manager) {
this.manager = manager;
this._content = '<input type="text" name="q" id="search_autocomplete" autocomplete="off" spellcheck="false" autofocus><div id="account_results"></div>' ;
this._input = '#search_autocomplete';
this._results_container = '#account_results';
this._init_outer_events();
}
open() {
var that = this;
this._$dialog = $.dialog({
title: 'Recherche de compte',
content: this._content,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
onOpen: function() {
that._$input = $(that._input);
that._$results_container = $(that._results_container);
that._init_form()
._init_inner_events();
},
});
}
_init_form() {
var that = this;
this._$input.yourlabsAutocomplete({
url: '{% url "kfet.account.search.autocomplete" %}',
minimumCharacters: 2,
id: 'search_autocomplete',
choiceSelector: '.choice',
placeholder: "Chercher un utilisateur K-Fêt",
container: that._$results_container,
box: that._$results_container,
});
return this;
}
_init_outer_events() {
var that = this;
/* open search on button click */
this.manager._$container
.on('click', '.search', () => this.open());
/* open search on Ctrl-F */
this.manager._$container
.on('keydown', function(e) {
if (e.which == 70 && e.ctrlKey) {
that.open();
e.preventDefault() ;
}
});
}
_init_inner_events() {
this._$input.bind('selectChoice',
(e, choice, autocomplete) => this._on_select(e, choice, autocomplete));
return this;
}
_on_select(e, choice, autocomplete) {
this.manager.update(choice.find('.trigramme').text());
this.reset();
}
reset() {
if (this._$dialog !== undefined) {
this._$dialog.close();
}
}
}
class CheckoutManager {
constructor() {
this._$container = $('#checkout');
this.display_prefix = '#checkout-';
this.checkout = new Checkout();
this.selection = new CheckoutSelection(this);
this._$laststatement_container = $('#last_statement');
this.laststatement = new Statement();
this.laststatement_display_prefix = '#checkout-last_statement_';
}
update(id) {
if (id !== undefined)
this.selection.set(id);
id = id || this.selection.get();
var api_options = {
'last_statement': true,
};
this.checkout.get_by_apipk(id, api_options,
(data) => this._update_on_success(data),
() => this.reset_data());
if (kpsul.account_manager.is_empty()) {
kpsul.account_manager.focus();
} else {
kpsul._env.articleSelect.focus().select();
}
}
_update_on_success(data) {
if (data['laststatement'] !== undefined)
this.laststatement.from(data['laststatement']);
$('#id_checkout').val(this.checkout.id);
this.display();
}
is_empty() { return this.checkout.is_empty(); }
display() {
this._display_data();
this._display_laststatement();
this._display_buttons();
}
_display_data() {
this.checkout.display(this._$container, {
'prefix_prop': this.display_prefix,
});
}
_display_laststatement() {
if (this.laststatement.is_empty()) {
this._$laststatement_container.hide();
} else {
this.laststatement.display(this._$laststatement_container, {
'prefix_prop': this.laststatement_display_prefix
});
this._$laststatement_container.show();
}
}
_display_buttons() {
var buttons = '';
if (!this.is_empty()) {
var id = this.checkout.id;
var url_details = Urls['kfet.checkout.read'](id);
var url_newcheckout = Urls['kfet.checkoutstatement.create'](id);
buttons += '<a href="' + url_newcheckout + '" title="Effectuer un relevé" class="btn btn-primary" target="_blank"><span class="glyphicon glyphicon-euro"></span></a>';
buttons += '<a class="btn btn-primary" href="' + url_details + '" title="Modifier" target="_blank"><span class="glyphicon glyphicon-cog"></span></a>';
}
this._$container.find('.buttons').html(buttons);
}
reset() {
$('#id_checkout').val(0);
this.selection.reset();
this.reset_data();
if (this.selection.choices.length == 1)
this.update(this.selection.choices[0]);
}
reset_data() {
this.checkout.clear();
this.laststatement.clear();
this.display();
}
}
class CheckoutSelection {
constructor(manager) {
this.manager = manager;
this._$input = $('#id_checkout_select');
this._init_events();
this.choices = [
for (option of this._$input.find('option'))
if ($(option).attr('value') != '')
parseInt($(option).attr('value'))
];
}
_init_events() {
this._$input.on('change', () => this.manager.update());
}
get() {
return this._$input.val() || 0;
}
set(v) {
this._$input.find('option[value='+ v +']').prop('selected', true);
}
reset() {
this._$input.find('option:first').prop('selected', true);
}
}

View file

@ -12,8 +12,11 @@
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% url 'js_reverse' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kpsul.js' %}"></script>
{% endblock %} {% endblock %}
{% block title %}K-Psul{% endblock %} {% block title %}K-Psul{% endblock %}
@ -80,7 +83,7 @@
{{ trigramme_form.trigramme }} {{ trigramme_form.trigramme }}
</div> </div>
<div class="col-lg-9 col-xs-8" id="account_data"> <div class="col-lg-9 col-xs-8" id="account_data">
<div id="account-balance" class="data_line"></div> <div id="account-balance_ukf" class="data_line"></div>
<div id="account-name" class="data_line"></div> <div id="account-name" class="data_line"></div>
<div id="account-nickname" class="data_line"></div> <div id="account-nickname" class="data_line"></div>
<div class="data_line"> <div class="data_line">
@ -102,7 +105,9 @@
<div> <div>
<b>En caisse:</b> <span id="checkout-balance"></span> <b>En caisse:</b> <span id="checkout-balance"></span>
</div> </div>
<div id="last_statement"> <div id="last_statement" style="display:none">
<b>Dernier relevé:</b><br>
<span id="checkout-last_statement_balance_new"></span>€ le <span id="checkout-last_statement_at"></span> par <span id="checkout-last_statement_by"></span>
</div> </div>
<div class="buttons"> <div class="buttons">
</div> </div>
@ -184,413 +189,6 @@ function booleanCheck(v) {
return v == true; return v == true;
} }
function functionCheck(v) {
if (typeof v === 'function')
return v;
return function(){};
}
class Config {
static getAll() {
if (typeof window.config === 'undefined')
window.config = {};
return window.config;
}
static reset(callback) {
$.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.get_settings' %}",
method : "POST",
})
.done(function(data) {
Config.addcost_for = data['addcost_for'];
Config.addcost_amount = data['addcost_amount'];
Config.subvention_cof = data['subvention_cof'];
})
.always(callback);
}
static set addcost_for(v) {
Config.getAll().addcost_for = v;
}
static set addcost_amount(v) {
Config.getAll().addcost_amount = floatCheck(v);
}
static set subvention_cof(v) {
Config.getAll().subvention_cof = floatCheck(v);
}
}
String.prototype.formatTri = function() {
return this.toUpperCase();
}
String.prototype.isValidTri = function() {
var pattern = /^[^a-z]{3}$/;
return pattern.test(this);
}
String.prototype.urlify = function() {
return encodeURIComponent(this);
}
class Account {
constructor() {
$.extend(this, this.constructor.default_data);
}
static get default_data() {
return {
'id': 0, 'trigramme': '', 'name': '', 'nickname': '', 'email': '',
'is_cof' : false, 'promo': '', 'balance': 0, 'is_frozen': false,
'departement': '',
};
};
get id() { return this._id; }
get balance() { return this._balance; }
get balance_ukf() { return amountToUKF(this.balance, this.is_cof); }
get is_cof() { return this._is_cof; }
get is_frozen() { return this._is_frozen; }
set id(v) { this._id = intCheck(v); }
set balance(v) { this._balance = floatCheck(v); }
set is_cof(v) { this._is_cof = booleanCheck(v); }
set is_frozen(v) { this._is_frozen = booleanCheck(v); }
from(data, callback) {
callback = functionCheck(callback);
$.extend(this, Account.default_data, data);
callback();
}
fromAPI(trigramme, on_success, on_error) {
on_error = functionCheck(on_error);
var that = this;
$.ajax({
dataType: "json",
url : "{% url 'kfet.account.read.json' %}",
method : "POST",
data : { trigramme: trigramme },
})
.done((data) => this.from(data, on_success))
.fail(on_error);
}
reset() {
this.from({});
}
}
class AccountFormatter {
constructor(account) {
this._account = account;
}
static get _data_balance_default() { return ''; }
static get _data_balance_frozen() { return 'frozen'; }
static get _data_balance_ok() { return 'ok'; }
static get _data_balance_low() { return 'low'; }
static get _data_balance_neg() { return 'neg'; }
get str_balance_ukf() { return ''; }
get str_is_cof() { return ''; }
get data_balance() { return this.constructor._data_balance_default; }
display($container) {
var a = this._account;
$container
.find('#account-balance')
.text(this.str_balance_ukf)
.end()
.find('#account-is_cof')
.text(this.str_is_cof)
.end()
.find('#account-name')
.text(a.name)
.end()
.find('#account-nickname')
.text(a.nickname)
.end()
.find('#account-departement')
.text(a.departement)
.end()
.find('#account-promo')
.text(a.promo)
.end()
.find('#account-email')
.text(a.email)
.end();
$container.attr('data-balance', this.data_balance);
}
}
class StandardAccountFormatter extends AccountFormatter {
get str_balance_ukf() {
return this._account.balance_ukf;
}
get str_is_cof() {
return this._account.is_cof ? 'COF' : 'Non-COF';
}
get data_balance() {
var is_frozen = this._account.is_frozen;
var balance = this._account.balance;
if (is_frozen) { return this.constructor._data_balance_frozen; }
else if (balance >= 5) { return this.constructor._data_balance_ok; }
else if (balance >= 0) { return this.constructor._data_balance_low; }
else /* balance < 0 */ { return this.constructor._data_balance_neg; }
}
}
class LIQAccountFormatter extends AccountFormatter {
get str_balance_ukf() { return ''; }
get str_is_cof() { return ''; }
get data_balance() { return this.constructor.data_balance_ok; }
}
class AccountManager {
constructor(env) {
this._env = env; // temporary, should be a link to "kpsul_manager" or something like this
this._$container = $('#account');
this.account = new Account();
this.selection = new AccountSelection(this);
this.search = new AccountSearch(this);
}
get is_empty() {
return this.account.id == 0;
}
get formatter() {
if (this.account.trigramme == 'LIQ') { return LIQAccountFormatter; }
else if (!this.is_empty) { return StandardAccountFormatter; }
else /* account is empty */ { return AccountFormatter; }
}
display() {
this._display_data();
this._display_buttons();
}
_display_data() {
(new (this.formatter)(this.account)).display(this._$container);
}
_display_buttons() {
// dirty
var buttons = '';
if (this.is_empty) {
var trigramme = this.selection.get();
if (trigramme.isValidTri()) {
var url_base = "{% url 'kfet.account.create' %}";
var url = url_base + '?trigramme=' + trigramme.urlify();
buttons += '<a href="'+url+'" class="btn btn-primary" target="_blank" title="Créer ce compte"><span class="glyphicon glyphicon-plus"></span></a>';
} else { /* trigramme input is empty or invalid */
buttons += '<button class="btn btn-primary search" title="Rechercher"><span class="glyphicon glyphicon-search"></span></button>';
}
} else { /* an account is loaded */
var url_pattern = "{% url 'kfet.account.read' 'LIQ' %}";
var url_base = url_pattern.substr(0, url_pattern.length - 3); /* dirty */
var url = url_base + this.account.trigramme.urlify();
buttons += '<a href="'+url+'" class="btn btn-primary" target="_blank" title="Modifier ce compte"><span class="glyphicon glyphicon-cog"></span></a>';
}
this._$container.find('.buttons').html(buttons);
}
update(trigramme) {
if (typeof trigramme !== 'undefined')
this.selection.set(trigramme);
trigramme = trigramme || this.selection.get();
if (trigramme.isValidTri()) {
this.account.fromAPI(trigramme,
() => this._update_on_success(),
() => this.reset_data()
);
} else {
this.reset_data();
}
}
_update_on_success() {
$('#id_on_acc').val(this.account.id);
this.display();
this._env.articleSelect.focus();
this._env.updateBasketAmount();
this._env.updateBasketRel();
}
reset() {
$('#id_on_acc').val(0);
this.selection.reset();
this.reset_data();
}
reset_data() {
this.account.reset();
this.display();
}
focus() {
this.selection.focus();
return this;
}
}
class AccountSearch {
constructor(manager) {
this.manager = manager;
this._content = '<input type="text" name="q" id="search_autocomplete" autocomplete="off" spellcheck="false" autofocus><div id="account_results"></div>' ;
this._input = '#search_autocomplete';
this._results_container = '#account_results';
this._init_outer_events();
}
open() {
console.log('plop');
var that = this;
this._$dialog = $.dialog({
title: 'Recherche de compte',
content: this._content,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
onOpen: function() {
that._$input = $(that._input);
that._$results_container = $(that._results_container);
that
._init_form();
._init_inner_events();
}
});
}
_init_form() {
var that = this;
this._$input.yourlabsAutocomplete({
url: '{% url "kfet.account.search.autocomplete" %}',
minimumCharacters: 2,
id: 'search_autocomplete',
choiceSelector: '.choice',
placeholder: "Chercher un utilisateur K-Fêt",
container: that._$results_container,
box: that._$results_container,
});
return this;
}
_init_outer_events() {
var that = this;
/* open search on button click */
this.manager._$container
.on('click', '.search', () => this.open());
/* open search on Ctrl-F */
this.manager._$container
.on('keydown', function(e) {
if (e.which == 70 && e.ctrlKey) {
that.open();
e.preventDefault() ;
}
});
}
_init_inner_events() {
this._$input
.bind('selectChoice', (e, choice, autocomplete) => this._on_select(e, choice, autocomplete));
return this;
}
_on_select(e, choice, autocomplete) {
this.manager.update(choice.find('.trigramme').text());
this.close();
}
close() {
this._$dialog.close();
}
}
class AccountSelection {
constructor(manager) {
this.manager = manager;
this._$input = $('#id_trigramme');
this._init_events();
}
_init_events() {
var that = this;
// user change trigramme
this._$input
.on('input', () => this.manager.update());
// LIQ shortcuts
this._$input
.on('keydown', function(e) {
// keys: 13:Enter|40:Arrow-Down
if (e.keyCode == 13 || e.keyCode == 40)
that.manager.update('LIQ');
});
}
get() {
return this._$input.val().formatTri();
}
set(v) {
this._$input.val(v);
}
focus() {
this._$input.focus();
}
reset() {
this.set('');
}
}
$(document).ready(function() { $(document).ready(function() {
'use strict'; 'use strict';
@ -601,91 +199,6 @@ $(document).ready(function() {
// Lock to avoid multiple requests // Lock to avoid multiple requests
var lock = 0; var lock = 0;
// -----
// Checkout data management
// -----
// Initializing
var checkout_container = $('#checkout');
var checkoutInput = $('#id_checkout_select');
var checkout_data = {}
var checkout_data_default = {
'id' : 0,
'name': '',
'balance' : '',
'valid_from': '',
'valid_to' : '',
'last_statement_balance': '',
'last_statement_at' : '',
'last_statement_by_trigramme' : '',
'last_statement_by_first_name': '',
'last_statement_by_last_name' : '' ,
}
var last_statement_container = $('#last_statement');
var last_statement_html_default = '<b>Dernier relevé: </b><br><span id="checkout-last_statement_balance"></span>€ le <span id="checkout-last_statement_at"></span> par <span id="checkout-last_statement_by_trigramme"></span>';
// Display data
function displayCheckoutData() {
var at_formated = '';
if (checkout_data['last_statement_at']) {
last_statement_container.html(last_statement_html_default);
var at = moment.tz(checkout_data['last_statement_at'], 'UTC');
at_formated = at.tz('Europe/Paris').format('DD/MM/YY à HH:mm');
} else {
last_statement_container.html('');
}
for (var elem in checkout_data) {
$('#checkout-'+elem).text(checkout_data[elem]);
}
$('#checkout-last_statement_at').text(at_formated);
var buttons = '';
if (checkout_data['id'] !== 0) {
var url_base = "{% url 'kfet.checkoutstatement.create' 1 %}";
url_base = url_base.substr(0,url_base.length - 16);
buttons += '<a class="btn btn-primary" href="'+url_base+checkout_data['id']+'/statements/add" title="Effectuer un relevé" target="_blank"><span class="glyphicon glyphicon-euro"></span></a>';
buttons += '<a class="btn btn-primary" href="'+url_base+checkout_data['id']+'" title="Modifier" target="_blank"><span class="glyphicon glyphicon-cog"></span></a>';
}
checkout_container.find('.buttons').html(buttons);
}
// Clear data
function resetCheckout() {
checkout_data = checkout_data_default;
$('#id_checkout').val(0);
checkoutInput.find('option:first').prop('selected', true);
displayCheckoutData();
}
// Store data
function storeCheckoutData(data) {
checkout_data = $.extend({}, checkout_data_default, data);
$('#id_checkout').val(checkout_data['id']);
displayCheckoutData();
}
// Retrieve data
function retrieveCheckoutData(id) {
$.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.checkout_data' %}",
method : "POST",
data : { 'pk': id },
})
.done(function(data) { storeCheckoutData(data) })
.fail(function() { resetCheckout() });
}
// Event listener
checkoutInput.on('change', function() {
retrieveCheckoutData(checkoutInput.val());
if (!account_manager.is_empty()) {
articleSelect.focus().select();
} else {
account_manager.focus();
}
});
// ----- // -----
// Auth // Auth
// ----- // -----
@ -1047,13 +560,12 @@ $(document).ready(function() {
var arrowKeys = /^(37|38|39|40)$/; var arrowKeys = /^(37|38|39|40)$/;
function amountEuroPurchase(article_data, nb) { function amountEuroPurchase(article_data, nb) {
var cfg = Config.getAll();
var amount_euro = - article_data[3] * nb ; var amount_euro = - article_data[3] * nb ;
if (cfg.addcost_for && cfg.addcost_amount && account_manager.account.trigramme != cfg.addcost_for) if (Config.get('addcost_for') && Config.get('addcost_amount') && account_manager.account.trigramme != Config.get('addcost_for'))
amount_euro -= cfg.addcost_amount * nb; amount_euro -= Config.get('addcost_amount') * nb;
var reduc_divisor = 1; var reduc_divisor = 1;
if (account_manager.account.is_cof) if (kpsul.account_manager.account.is_cof)
reduc_divisor = 1 + cfg.subvention_cof / 100; reduc_divisor = 1 + Config.get('subvention_cof') / 100;
return amount_euro / reduc_divisor; return amount_euro / reduc_divisor;
} }
@ -1070,7 +582,7 @@ $(document).ready(function() {
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(nb).end() .find('.number').text(nb).end()
.find('.name').text(article_data[0]).end() .find('.name').text(article_data[0]).end()
.find('.amount').text(amountToUKF(amount_euro, account_manager.account.is_cof)); .find('.amount').text(amountToUKF(amount_euro, kpsul.account_manager.account.is_cof));
basket_container.prepend(article_basket_html); basket_container.prepend(article_basket_html);
updateBasketRel(); updateBasketRel();
} }
@ -1084,7 +596,7 @@ $(document).ready(function() {
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(amount+"€").end() .find('.number').text(amount+"€").end()
.find('.name').text(text).end() .find('.name').text(text).end()
.find('.amount').text(amountToUKF(amount, account_manager.account.is_cof)); .find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof));
basket_container.prepend(deposit_basket_html); basket_container.prepend(deposit_basket_html);
updateBasketRel(); updateBasketRel();
} }
@ -1097,7 +609,7 @@ $(document).ready(function() {
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(amount+"€").end() .find('.number').text(amount+"€").end()
.find('.name').text('Retrait').end() .find('.name').text('Retrait').end()
.find('.amount').text(amountToUKF(amount, account_manager.account.is_cof)); .find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof));
basket_container.prepend(withdraw_basket_html); basket_container.prepend(withdraw_basket_html);
updateBasketRel(); updateBasketRel();
} }
@ -1139,7 +651,7 @@ $(document).ready(function() {
var amount = $(this).find('#id_form-'+opeindex+'-amount'); var amount = $(this).find('#id_form-'+opeindex+'-amount');
if (!deleted && type == "purchase") if (!deleted && type == "purchase")
amount.val(amountEuroPurchase(article_id, article_nb)); amount.val(amountEuroPurchase(article_id, article_nb));
basket_container.find('[data-opeindex='+opeindex+'] .amount').text(amountToUKF(amount.val(), account_manager.account.is_cof)); basket_container.find('[data-opeindex='+opeindex+'] .amount').text(amountToUKF(amount.val(), kpsul.account_manager.account.is_cof));
}); });
} }
@ -1147,8 +659,9 @@ $(document).ready(function() {
function updateBasketRel() { function updateBasketRel() {
var basketrel_html = ''; var basketrel_html = '';
var trigramme = account_manager.account.trigramme; var account = kpsul.account_manager.account;
var is_cof = account_manager.account.is_cof; var trigramme = account.trigramme;
var is_cof = account.is_cof;
if (trigramme == 'LIQ' && !isBasketEmpty()) { if (trigramme == 'LIQ' && !isBasketEmpty()) {
var amount = - getAmountBasket(); var amount = - getAmountBasket();
basketrel_html += '<div>Total: '+amount.toFixed(2)+' €</div>'; basketrel_html += '<div>Total: '+amount.toFixed(2)+' €</div>';
@ -1161,7 +674,7 @@ $(document).ready(function() {
} else if (trigramme != '' && !isBasketEmpty()) { } else if (trigramme != '' && !isBasketEmpty()) {
var amount = getAmountBasket(); var amount = getAmountBasket();
var amountUKF = amountToUKF(amount, is_cof); var amountUKF = amountToUKF(amount, is_cof);
var newBalance = account_manager.account.balance + amount; var newBalance = account.balance + amount;
var newBalanceUKF = amountToUKF(newBalance, is_cof); var newBalanceUKF = amountToUKF(newBalance, is_cof);
basketrel_html += '<div>Total: '+amountUKF+'</div>'; basketrel_html += '<div>Total: '+amountUKF+'</div>';
basketrel_html += '<div>Nouveau solde: '+newBalanceUKF+'</div>'; basketrel_html += '<div>Nouveau solde: '+newBalanceUKF+'</div>';
@ -1340,7 +853,7 @@ $(document).ready(function() {
function updatePreviousOp() { function updatePreviousOp() {
var previousop_html = ''; var previousop_html = '';
var trigramme = account_manager.account.trigramme; var trigramme = kpsul.account_manager.account.trigramme;
previousop_html += '<div class="trigramme">Trigramme : '+trigramme+'</div>'; previousop_html += '<div class="trigramme">Trigramme : '+trigramme+'</div>';
previousop_html += basketrel_container.html(); previousop_html += basketrel_container.html();
previousop_container.html(previousop_html); previousop_container.html(previousop_html);
@ -1357,14 +870,13 @@ $(document).ready(function() {
var addcost_default_html = '<div id="addcost">Majoration vers <span class="addcost_for"></span> de <span class="addcost_amount"></span></div>'; var addcost_default_html = '<div id="addcost">Majoration vers <span class="addcost_for"></span> de <span class="addcost_amount"></span></div>';
function displayAddcost() { function displayAddcost() {
var cfg = Config.getAll(); $('#addcost').remove();
checkout_container.find('#addcost').remove();
$('body').css('animation', 'none'); $('body').css('animation', 'none');
if (cfg.addcost_for && cfg.addcost_amount) { if (Config.get('addcost_for') && Config.get('addcost_amount')) {
var addcost_html = $(addcost_default_html); var addcost_html = $(addcost_default_html);
addcost_html addcost_html
.find('.addcost_for').text(cfg.addcost_for).end() .find('.addcost_for').text(Config.get('addcost_for')).end()
.find('.addcost_amount').text(cfg.addcost_amount.toFixed(2)).end(); .find('.addcost_amount').text(Config.get('addcost_amount').toFixed(2)).end();
checkout_container.prepend(addcost_html); checkout_container.prepend(addcost_html);
$('body').css('animation', 'colorchange 3s infinite'); $('body').css('animation', 'colorchange 3s infinite');
} }
@ -1494,8 +1006,8 @@ $(document).ready(function() {
.text(article['stock']); .text(article['stock']);
} }
if (data['addcost']) { if (data['addcost']) {
Config.addcost_for = data['addcost']['for']; Config.set('addcost_for', data['addcost']['for']);
Config.addcost_amount = data['addcost']['amount']; Config.set('addcost_amount', data['addcost']['amount']);
displayAddcost(); displayAddcost();
} }
} }
@ -1507,17 +1019,17 @@ $(document).ready(function() {
// Reset functions // Reset functions
function coolReset(give_tri_focus=true) { function coolReset(give_tri_focus=true) {
account_manager.reset(); kpsul.account_manager.reset();
resetBasket(); resetBasket();
resetComment(); resetComment();
resetSelectable(); resetSelectable();
if (give_tri_focus) if (give_tri_focus)
account_manager.focus(); kpsul.account_manager.focus();
} }
function hardReset(give_tri_focus=true) { function hardReset(give_tri_focus=true) {
coolReset(give_tri_focus); coolReset(give_tri_focus);
checkoutInput.trigger('change'); kpsul.checkout_manager.reset();
resetArticles(); resetArticles();
resetPreviousOp(); resetPreviousOp();
khistory.reset(); khistory.reset();
@ -1605,7 +1117,7 @@ $(document).ready(function() {
updateBasketRel: updateBasketRel, updateBasketRel: updateBasketRel,
}; };
var account_manager = new AccountManager(env); window.kpsul = new KPsulManager(env);
hardReset(); hardReset();

View file

@ -132,8 +132,8 @@ urlpatterns = [
# ----- # -----
url('^k-psul/$', views.kpsul, name='kfet.kpsul'), url('^k-psul/$', views.kpsul, name='kfet.kpsul'),
url('^k-psul/checkout_data$', views.kpsul_checkout_data, url(r'^k-psul/checkout_data/(?P<pk>\d+)$', views.kpsul_checkout_data,
name='kfet.kpsul.checkout_data'), name='kfet.kpsul.checkout_data.read'),
url('^k-psul/perform_operations$', views.kpsul_perform_operations, url('^k-psul/perform_operations$', views.kpsul_perform_operations,
name='kfet.kpsul.perform_operations'), name='kfet.kpsul.perform_operations'),
url('^k-psul/cancel_operations$', views.kpsul_cancel_operations, url('^k-psul/cancel_operations$', views.kpsul_cancel_operations,
@ -151,9 +151,6 @@ urlpatterns = [
url(r'^history.json$', views.history_json, url(r'^history.json$', views.history_json,
name='kfet.history.json'), name='kfet.history.json'),
url(r'^accounts/read.json$', views.account_read_json,
name='kfet.account.read.json'),
# ----- # -----
# Settings urls # Settings urls

View file

@ -17,6 +17,7 @@ from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User, Permission, Group from django.contrib.auth.models import User, Permission, Group
from django.http import HttpResponse, JsonResponse, Http404 from django.http import HttpResponse, JsonResponse, Http404
from django.forms import modelformset_factory, formset_factory from django.forms import modelformset_factory, formset_factory
from django.forms.models import model_to_dict
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.db.models import F, Sum, Prefetch, Count, Func from django.db.models import F, Sum, Prefetch, Count, Func
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
@ -316,12 +317,14 @@ def account_create_ajax(request, username=None, login_clipper=None):
'user_form' : forms['user_form'], 'user_form' : forms['user_form'],
}) })
# Account - Read # Account - Read
@login_required @login_required
def account_read(request, trigramme): def account_read(request, trigramme):
try: try:
account = Account.objects.select_related('negative').get(trigramme=trigramme) account = Account.objects.select_related('negative')\
.get(trigramme=trigramme)
except Account.DoesNotExist: except Account.DoesNotExist:
raise Http404 raise Http404
@ -330,19 +333,32 @@ def account_read(request, trigramme):
and request.user != account.user: and request.user != account.user:
raise PermissionDenied raise PermissionDenied
addcosts = (OperationGroup.objects if request.GET.get('format') == 'json':
.filter(opes__addcost_for=account,opes__canceled_at=None) data = model_to_dict(
.extra({'date':"date(at)"}) account,
.values('date') fields=['id', 'trigramme', 'firstname', 'lastname', 'email',
.annotate(sum_addcosts=Sum('opes__addcost_amount')) 'is_cof', 'promo', 'balance', 'is_frozen', 'departement',
.order_by('-date')) 'nickname', 'trigramme']
)
data['name'] = account.name
return JsonResponse(data)
addcosts = (
OperationGroup.objects
.filter(opes__addcost_for=account, opes__canceled_at=None)
.extra({'date': "date(at)"})
.values('date')
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
.order_by('-date')
)
return render(request, "kfet/account_read.html", { return render(request, "kfet/account_read.html", {
'account' : account, 'account': account,
'addcosts': addcosts, 'addcosts': addcosts,
'settings': { 'subvention_cof': Settings.SUBVENTION_COF() }, 'settings': {'subvention_cof': Settings.SUBVENTION_COF()},
}) })
# Account - Update # Account - Update
@login_required @login_required
@ -827,45 +843,28 @@ def kpsul_get_settings(request):
} }
return JsonResponse(data) return JsonResponse(data)
@teamkfet_required
def account_read_json(request):
trigramme = request.POST.get('trigramme', '')
account = get_object_or_404(Account, trigramme=trigramme)
data = { 'id': account.pk, 'name': account.name, 'email': account.email,
'is_cof': account.is_cof, 'promo': account.promo,
'balance': account.balance, 'is_frozen': account.is_frozen,
'departement': account.departement, 'nickname': account.nickname,
'trigramme': account.trigramme }
return JsonResponse(data)
@teamkfet_required @teamkfet_required
def kpsul_checkout_data(request): def kpsul_checkout_data(request, pk):
pk = request.POST.get('pk', 0) checkout = get_object_or_404(Checkout, pk=pk)
if not pk: data = model_to_dict(
pk = 0 checkout,
data = (Checkout.objects fields=['id', 'name', 'balance', 'valid_from', 'valid_to']
.annotate( )
last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'),
last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'), if request.GET.get('last_statement'):
last_statement_by_trigramme=F('statements__by__trigramme'), last_statement = checkout.statements.latest('at')
last_statement_balance=F('statements__balance_new'), last_statement_data = model_to_dict(
last_statement_at=F('statements__at')) last_statement,
.values( fields=['id', 'at', 'balance_new', 'balance_old', 'by']
'id', 'name', 'balance', 'valid_from', 'valid_to', )
'last_statement_balance', 'last_statement_at', # ``at`` is not editable, so skipped by ``model_to_dict``
'last_statement_by_trigramme', 'last_statement_by_last_name', last_statement_data['at'] = last_statement.at
'last_statement_by_first_name') data['laststatement'] = last_statement_data
.select_related(
'statements'
'statements__by',
'statements__by__cofprofile__user')
.filter(pk=pk)
.order_by('statements__at')
.last())
if data is None:
raise Http404
return JsonResponse(data) return JsonResponse(data)
@teamkfet_required @teamkfet_required
def kpsul_update_addcost(request): def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST) addcost_form = AddcostForm(request.POST)

View file

@ -18,3 +18,4 @@ statistics==1.0.3.5
future==0.15.2 future==0.15.2
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
git+https://github.com/Aureplop/channels.git#egg=channels git+https://github.com/Aureplop/channels.git#egg=channels
django-js-reverse==0.7.3