Warning: session_start(): open(/var/lib/php/sessions/sess_118a6d2ktu9jh4jl6v26uovis0, O_RDWR) failed: No space left on device (28) in /var/www/tb-usercake/models/config.php on line 51
Warning: session_start(): Failed to read session data: files (path: /var/lib/php/sessions) in /var/www/tb-usercake/models/config.php on line 51
// one instance should only be used for one page at a time
sclass HCRUD extends HAbstractRenderable {
HCRUD_Data data; // normally an instance of HCRUD_Concepts (#1026002)
// yeah these fields are a mess...
bool mutationRights = true;
bool allowCreateOrDelete = true;
bool allowCreate = true;
bool allowEdit = true;
bool singleton;
bool cmdsLeft; // put commands on the left instead of the right
bool showEntryCountInTitle;
bool allowFieldRenaming;
int defaultTextFieldCols = 80;
int valueDisplayLength = 1000;
S tableClass; // CSS class for table
S formTableClass; // CSS class for form
S checkBoxClass = "crud_chkbox";
S customTitle;
bool showTextFieldsAsAutoExpandingTextAreas;
Set unshownFields; // not shown in table or form
Set uneditableFields; // not shown in form
Set unlistedFields; // not shown in table
bool showCheckBoxes;
bool cleanItemIDs;
bool haveJQuery, haveSelectizeJS, haveSelectizeClickable;
bool needsJQuery; // unused
bool paginate;
bool sortable;
bool buttonsOnTop, buttonsOnBottom = true;
bool duplicateInNewTab;
S formID = "crudForm";
bool showQuickSaveButton; // needs hnotificationPopups()
bool enableMultiSelect = true; // enables shift+click on check boxes. needs haveJQuery
bool cellColumnToolTips; // give each table cell a tooltip showing its column name
bool showSearchField;
S searchQuery;
SS params;
new HTMLPaginator paginator;
// sort options
S sortByField = "id";
S sortParameter = "sort";
bool descending;
int flexibleLengthListLeeway = 5; // how many empty rows to display at the end of each flexible length list
// set internally after update/create or from "selectObj" param to highlight an item in the list (or show only that item)
O objectIDToHighlight;
bool showOnlySelected;
S fieldPrefix = "f_";
int entryCount;
*() {}
*(HCRUD_Data *data) {}
*(S *baseLink, HCRUD_Data *data) {}
S newLink() { ret appendQueryToURL(baseLink, cmd := "new"); }
S deleteLink(O id) { ret appendQueryToURL(baseLink, "delete_" + id, 1); }
S editLink(O id) { ret appendQueryToURL(baseLink, edit := id); }
S duplicateLink(O id) { ret appendQueryToURL(baseLink, duplicate := id); }
void setParams(SS params) {
this.params = params;
if (objectIDToHighlight == null) objectIDToHighlight = params.get("selectObj");
if (eq("1", params.get("showOnlySelected")))
set showOnlySelected;
}
// also handles commands if withCmds=true
// you probably want to call renderPage() instead to handle all commands
S render(bool withCmds, SS params) {
//print("HCRUD render");
setParams(params);
if (!withCmds) ret renderTable(false);
try answer handleCommands(params);
ret renderMsgs(params)
+ divUnlessEmpty(nav())
+ renderTable(withCmds);
}
swappable S nav() {
new LS l;
if (actuallyAllowCreate())
l.add(ahref(newLink(), "New " + itemName()));
if (showSearchField)
l.add(hInlineSearchForm("search", searchQuery, ""));
ret joinWithVBar(l);
}
S handleCommands(SS params) {
new LS msgs;
if (eqGet(params, "action", "create")) {
if (!actuallyAllowCreate()) fail("Creating objects not allowed");
processRenames(params);
O id = data.createObject(preprocessUpdateParams(params), fieldPrefix);
msgs.add(itemName() + " created (ID: " + id + ")");
objectIDToHighlight = id;
}
if (eqGet(params, "action", "update")) {
if (!actuallyAllowEdit()) fail("Editing objects not allowed");
S id = params.get("id");
processRenames(params);
msgs.add(data.updateObject(id, preprocessUpdateParams(params), fieldPrefix));
objectIDToHighlight = id;
}
LS toDeleteList = keysDeprefixNemptyValue(params, "delete_");
if (eq(params.get("bulkAction"), "deleteSelected"))
toDeleteList.addAll(keysDeprefixNemptyValue(params, "obj_"));
for (S toDelete : toDeleteList) {
if (!actuallyAllowDelete()) fail("Deleting objects not allowed");
msgs.add(data.deleteObject(toDelete));
}
ret empty(msgs) ? "" : refreshAfterCommand(params, msgs);
}
swappable S refreshAfterCommand(SS params, LS msgs) {
S redirectAfterSave = mapGet(params, "redirectAfterSave");
if (nempty(redirectAfterSave)) ret hrefresh(redirectAfterSave);
ret refreshWithMsgs(msgs,
anchor := objectIDToHighlight == null ? null : "obj" + objectIDToHighlight,
params := objectIDToHighlight == null ? null : litmap(selectObj := objectIDToHighlight));
}
S encodeField(S s) {
ret or(data.fieldNameToHTML(s), s);
}
// in table
swappable S renderValue(S field, O value) {
if (value cast HTML) ret value.html;
value = deref(value);
if (value cast SecretValue)
ret hhiddenStuff(renderValue_inner(value!));
ret renderValue_inner(value);
}
swappable S renderValue_inner(O value) {
if (value cast Bool)
ret yesNo_short(value);
ret htmlEncode_nlToBr_withIndents(shorten(valueDisplayLength, strOrEmpty(value)));
}
S renderTable(bool withCmds) {
ret renderTable(withCmds, data.list());
}
S valueToSortable(O value) {
if (value cast HTML)
ret value!;
ret strOrNull(value);
}
// l = list of maps as it comes from data object
S renderTable(bool withCmds, L l) {
//print("HCRUD renderTable");
entryCount = l(l);
if (empty(l)) ret p("No entries");
if (!eq(data.defaultSortField(), pair(sortByField, descending))) {
if (nempty(sortByField)) {
print("Sorting " + nEntries(l) + " by " + sortByField);
l = sortByTransformedMapKey_alphaNum valueToSortable(l, sortByField);
}
if (descending) l = reversed(l);
}
// l2 is the rendered map. use keyEncoding to get from l keys to l2 keys
new SS keyEncoding;
L l2 = map(l, _map -> {
O id = itemID(_map);
ret data.new Item(id) {
public MapSO calcFullMap() {
MapSO map = _map;
map = mapMinusKeys(map, joinSets(unshownFields, unlistedFields));
MapSO map2 = postProcessTableRow(map, mapToMap(
(key, value) -> pair(mapPut_returnValue(keyEncoding, key, encodeField(key)), renderValue(key, value)),
map));
if (singleton)
map2.remove(encodeField(data.idField())); // don't show ID in table in singleton mode
if (withCmds)
map2 = addCmdsToTableRow(map, map2);
// add anchor to row
map2.put(firstKey(map2), aname("obj" + id, firstValue(map2)));
ret map2;
}
};
});
new LS out;
if (paginate) {
paginator.processParams(params);
paginator.baseLink = addParamsToURL(baseLink,
filterKeys keepParamInPagination(params));
paginator.max = l(l2);
out.add(divUnlessEmpty(paginator.renderNav()));
L l3 = subListOrFull(l2, paginator.visibleRange());
//printVars_str(+objectIDToHighlight, visible := l(l3), first := first(l3), firstID := mapToID(first(l3)));
// if highlighted object is not on page or user wants to see only this object, show only this object
if (objectIDToHighlight != null && (showOnlySelected || !any isHighlighted(l3))) {
l3 = llNonNulls(firstThat isHighlighted(l2));
//printVars_str(+objectIDToHighlight, total := l(l2), found := l(l3));
}
l2 = l3;
}
new SS replaceHeaders;
if (sortable && !singleton)
for (S key, html : keyEncoding) {
bool sortedByField = eq(sortByField, key);
bool showDescendingLink = sortedByField && !descending;
S htmlOld = html;
if (sortedByField) {
S title = showDescendingLink ? "Click here to sort descending" : "Click here to sort ascending";
S titleSorted = "Sorted by this field ("
+ (descending ? "descending" : "ascending") + ")";
title = titleSorted + ". " + title;
html = span_title(title, unicode_downOrUpPointingTriangle(descending)) + " " + html;
}
S sortLink = appendQueryToURL(baseLink, sortParameter, showDescendingLink ? "-" + key : key);
replaceHeaders.put(htmlOld,
/*html + " " + ahref(sortLink,
unicode_smallDownOrUpPointingTriangle(showDescendingLink), +title)*/
ahref(sortLink, html));
}
Map paramsByColName = null;
if (cellColumnToolTips) {
paramsByColName = new Map;
for (MapSO map : l2)
for (S key : keys(map))
if (!paramsByColName.containsKey(key))
paramsByColName.put(key, litobjectarray(title := nullIfEmpty(htmldecode_dropTagsAndComments(key))));
}
out.add(hpostform(
htmlTable2_noHtmlEncode(l2, paramsPlus(tableParams(), +replaceHeaders, +paramsByColName))
+ (!withCmds || !showCheckBoxes ? "" : "\n" + divUnlessEmpty(renderBulkCmds())),
action := baseLink));
if (showCheckBoxes && haveJQuery && enableMultiSelect)
out.add(hCheckBoxMultiSelect_v2());
ret lines_rtrim(out);
}
O mapToID(MapSO item) {
ret item == null ?: dropAllTags(strOrNull(item.get(encodeField(idField()))));
}
// item is after encodeField
bool isHighlighted(MapSO item) {
O id = mapToID(item);
//print("isHighlighted ID: " + toStringWithClass(id) + " / " + toStringWithClass(objectIDToHighlight));
ret eq(id, objectIDToHighlight);
}
swappable S renderBulkCmds() {
ret "Bulk action: " + hselect("bulkAction", litorderedmap("" := "", "deleteSelected" := "Delete selected"))
+ " " + hsubmit("OK", onclick := "return confirm('Are you sure?')");
}
MapSO addCmdsToTableRow(MapSO map, MapSO map2) {
if (showCheckBoxes) {
O id = itemID(map);
map2.put(checkBoxKey(), hcheckbox("obj_" + id, false, title := "Select this object for a bulk action", class := checkBoxClass));
map2 = putKeysFirst(map2, checkBoxKey());
}
map2.put(cmdsKey(), renderCmds(map));
if (cmdsLeft)
map2 = putKeysFirst(map2, cmdsKey());
ret map2;
}
/*swappable*/ O[] tableParams() {
ret litparams(
tdParams := litparams(valign := "top"),
tableParams := litparams(class := tableClass));
}
S renderForm(MapSO map) {
temp tempSetTL(htmlencode_forParams_useV2, true);
//print("renderForm: filteredFields=" + data.filteredFields());
map = mapMinusKeys(map, joinSets(unshownFields, uneditableFields, data.filteredFields()));
MapSO mapWithoutID = mapWithoutKey(map, data.idField());
LLS matrix = map(mapWithoutID, (field, value) -> {
S help = data.fieldHelp(field);
ret ll(
allowFieldRenaming ? hinputfield("rename_" + field, field, class := "field-rename", style := "border: none; text-align: right", title := "Edit this to rename field " + quote(field) + " or clear to delete field") : encodeField(field),
addHelpText(help, renderInput(field, value))
);
});
massageFormMatrix(map, matrix);
ret htableRaw_valignTop(matrix
, empty(formTableClass) ? litparams(border := 1, cellpadding := 4) : litparams(class := formTableClass));
}
S renderInput(S field, O value) {
S name = fieldPrefix + field;
ret renderInput(name, data.getRenderer(field, value), value);
}
S renderInput(S name, HCRUD_Data.Renderer r, O value) {
//print("Renderer for " + name + ": " + r);
if (r != null) value = r.preprocessValue(value);
S meta = r == null ? "" : renderMetaInfo(r.metaInfo, name);
// switch by renderer type
if (r cast HCRUD_Data.AceEditor) {
//ret meta + hAceEditor(strOrEmpty(value), style := "width: " + r.cols + "ch; height: " + r.rows + "em", +name);
HTMLAceEditor ace = new(strOrEmpty(value));
ace.name = name;
ace.divParams.put(style := "width: " + r.cols + "ch; height: " + r.rows + "em");
customizeACEEditor(ace);
ret meta + ace.headStuff() + ace.html();
}
if (r cast HCRUD_Data.TextArea)
ret meta + htextarea(strOrEmpty(value), +name, cols := r.cols, rows := r.rows);
if (r cast HCRUD_Data.TextField)
ret meta + renderTextField(name, strOrEmpty(value), r.cols);
if (r cast HCRUD_Data.ComboBox)
ret meta
+ renderComboBox(name, r.valueToEntry(value), r.entries, r.editable);
if (r cast HCRUD_Data.DynamicComboBox)
ret meta
+ renderDynamicComboBox(name, r.valueToEntry(value), r.info, r.editable, r.url);
if (r cast HCRUD_Data.CheckBox)
//ret hcheckbox(name, isTrue(value));
ret meta + htrickcheckboxWithText(name, "", isTrue(value));
if (r cast HCRUD_Data.FlexibleLengthList) {
L list = cast value;
new LS rows;
int n = l(list)+flexibleLengthListLeeway;
for i to n: {
O item = _get(list, i);
//print("Item: " + item);
rows.add(tr(td(i+1 + ".", align := "right")
+ td(renderInput(name + "_" + i, r.itemRenderer, item))));
}
ret meta + htag table(lines(rows));
}
if (r instanceof HCRUD_Data.NotEditable)
ret "Not editable";
ret renderInput_default(name, value);
}
swappable void customizeACEEditor(HTMLAceEditor ace) {}
S renderMetaInfo(S metaInfo, S name) {
if (empty(metaInfo)) ret "";
ret hhidden("metaInfo_" + dropPrefix(fieldPrefix, name), metaInfo);
}
S renderInput_default(S name, O value) {
ret renderTextField(name, strOrEmpty(value), defaultTextFieldCols);
}
S renderTextField(S name, S value, int cols) {
if (showTextFieldsAsAutoExpandingTextAreas) {
ret htextarea(value, +name, class := "auto-expand",
style := "width: " + cols + "ch",
autofocus := eq(mapGet(params, "autofocus"), name) ? html_valueLessParam() : null,
onkeydown := jquery_submitFormOnCtrlEnter());
}
ret htextfield(name, value, size := cols, style := "font-family: monospace");
}
S renderNewForm() {
ret renderNewForm(data.emptyObject());
}
// pre-populate fields from request parameters
S renderNewFormWithParams(SS params) {
SS filteredMap = subMapStartingWith_dropPrefix(params, fieldPrefix);
MapSO map = joinMaps(data.emptyObject(), (Map) filteredMap);
data.rawFormValues = params;
// pre-populate list fields
for (S key, value : filteredMap) {
LS l = splitAt(key, "_");
if (l(l) == 2) {
S field = first(l);
int idx = parseInt(second(l));
LS list = cast map.get(field);
if (!list instanceof ArrayList) map.put(field, list = new L);
listPut(list, idx, value);
}
}
ret renderNewForm(map);
}
S renderNewForm(MapSO map) {
//printStruct("renderNewForm", map);
S buttons = p(hsubmit("Create"));
ret hpostform(
hhidden("action", "create")
+ formExtraHiddens()
+ stringIf(buttonsOnTop, buttons)
+ renderForm(map)
+ stringIf(buttonsOnBottom, buttons),
paramsPlus(formParameters(), action := baseLink));
}
swappable O[] formParameters() { ret litparams(id := formID); }
S idField() { ret data.idField(); }
S renderEditForm(S id) {
if (!actuallyAllowEdit())
ret "Can't edit objects in this table";
if (!data.objectCanBeEdited(id))
ret htmlEncode2("Object " + id + " can't be edited");
MapSO map = data.getObjectForEdit(id);
if (map == null) ret htmlEncode2("Entry " + id + " not found");
S onlyFields = mapGet(params, "onlyFields");
if (nempty(onlyFields))
map = onlyKeys(map, itemPlus(idField(), tok_identifiersOnly(onlyFields)));
S buttons = p_vbar(
hsubmit("Save changes"),
!showQuickSaveButton ? "" :
hbuttonOnClick_noSubmit("Save & keep editing", [[
$.ajax({
type: 'POST',
url: $('#crudForm').attr('action'),
data: $('#crudForm').serialize(),
success: function(response) { successNotification("Saved"); },
}).error(function() { errorNotification("Couldn't save"); });
]]),
deleteObjectHTML(id));
ret hpostform(
hhidden("action", "update") +
formExtraHiddens() +
hhidden(+id) +
p("Object ID: " + htmlEncode2(id)) +
+ stringIf(buttonsOnTop, buttons)
+ renderForm(map)
+ stringIf(buttonsOnBottom, buttons),
paramsPlus(formParameters(), action := baseLink + "#obj" + id));
}
S renderPage(SS params) {
//print("HCRUD renderPage");
setParams(params);
try answer handleComboSearch(params);
if (eqGet(params, "cmd", "new")) {
if (!actuallyAllowCreate())
ret "Can't create objects in ths table";
ret frame(customTitleOr("New " + itemName()), renderNewFormWithParams(params));
}
if (nempty(params.get("edit")))
ret frame("Edit " + itemName(), renderEditForm(params.get("edit")));
if (nempty(params.get("duplicate")))
ret frame("New " + itemName(), renderNewForm(data.getObjectForDuplication(params.get("duplicate"))));
S rendered = render(mutationRights, params);
// handle commands, render list
S title = null;
if (singleton)
title = ahref(baseLink, firstToUpper(data.itemName()));
else {
if (objectIDToHighlight != null)
title = data.titleForObjectID(objectIDToHighlight);
if (empty(title))
title = (showEntryCountInTitle ? n2(entryCount) + " " : "")
+ ahref(baseLink, firstToUpper(data.itemNamePlural()));
}
ret frame(customTitleOr(title), rendered);
}
HCRUD makeFrame(MakeFrame makeFrame) { super.makeFrame(makeFrame); this; }
S cmdsKey() { ret ""; }
S checkBoxKey() { ret ""; }
S itemName() { ret data.itemName(); }
swappable MapSO postProcessTableRow(MapSO data, MapSO rendered) { ret rendered; }
O itemID(MapSO item) {
O id = mapGet(item, data.idField());
// getVarOpt decodes HTML record
if (cleanItemIDs) id = htmlDecode_dropTags(strOrNull(getVarOpt(id)));
ret id;
}
long itemIDAsLong(MapSO item) {
ret parseLong(itemID(item));
}
// return list of HTMLs for commands in pop down button
swappable LS additionalCmds(MapSO item) { null; }
swappable S renderCmds(MapSO item) {
O id = itemID(item);
LS additionalCmds = additionalCmds(item);
ret joinNemptiesWithVBar(
!actuallyAllowEdit() || !data.objectCanBeEdited(id) ? null : ahref(editLink(id), "EDIT"),
deleteObjectHTML(id),
!actuallyAllowCreate() ? null : targetBlankIf(duplicateInNewTab, duplicateLink(id), "dup", title := "duplicate"),
empty(additionalCmds) ? null : hPopDownButton(additionalCmds)
);
}
bool actuallyAllowCreate() {
ret !singleton && allowCreateOrDelete && allowCreate;
}
bool actuallyAllowEdit() {
ret allowCreateOrDelete && allowEdit;
}
bool actuallyAllowDelete() {
ret !singleton && allowCreateOrDelete;
}
// e.g. for adding rows to edit/create form
swappable void massageFormMatrix(MapSO map, LLS matrix) {
}
S renderComboBox(S name, S value, LS entries, bool editable) {
if (haveSelectizeJS) {
// coolest option - use selectize.js
S id = aGlobalID();
ret hselect_list(entries, value, +name, +id)
+ hjs("$('#" + id + "').selectize" + [[
({
searchField: 'text',
openOnFocus: true,
dropdownParent: 'body',
create: ]] + jsBool(editable) + [[
/*allowEmptyOption: true*/
]]
+ unnull(moreSelectizeOptions2(name)) + [[
});
]])
+ selectizeLayoutFix();
}
if (haveJQuery) {
// make searchable list if JQuery is available
// (functional, but looks quite poor on Firefox)
// TODO: this seems to always be editable
S id = aGlobalID();
ret tag datalist(mapToLines hoption(entries), +id)
+ tag input("", +name, list := id);
}
// standard non-searchable list
if (editable)
ret hinputfield(name, value); // no editable combo box possible without selectize.js
ret hselect_list(entries, value, +name);
}
S renderDynamicComboBox(S name, S value, S info, bool editable, S url default null) {
assertTrue(+haveSelectizeJS);
S id = aGlobalID();
S ajaxURL = or2(url, baseLink);
ret hselect_list(llNempties(value), value, +name, +id)
+ hjs("$('#" + id + "').selectize" + [[
({
searchField: 'text',
valueField: 'text',
labelField: 'text',
openOnFocus: true,
dropdownParent: 'body',
create: ]] + jsBool(editable) + [[,
load: function(query, callback) {
if (!query.length) return callback();
var data = {
comboSearchInfo: ]] + jsQuote(info) + [[,
comboSearchQuery: query
};
console.log("Loading " + ]] + jsQuote(baseLink) + [[ + " with " + JSON.stringify(data));
$.ajax({
url: ]] + jsQuote(ajaxURL) + [[,
type: 'GET',
dataType: 'json',
data: data,
error: function() {
console.log("Got error");
callback();
},
success: function(res) {
//console.log("Got data: " + res);
var converted = res.map(x => { return {text: x}; });
//console.log("Converted: " + converted);
callback(converted);
}
});
}
/*allowEmptyOption: true*/
]] + moreSelectizeOptions2(name) + [[
});
]])
+ selectizeLayoutFix();
}
void processSortParameter(SS params) {
S sort = mapGet(params, sortParameter);
//sortByField = null;
if (nempty(sort))
if (startsWith(sort, "-")) {
descending = true;
sortByField = substring(sort, 1);
} else {
descending = false;
sortByField = sort;
}
}
S deleteObjectHTML(O id) {
ret !actuallyAllowDelete() ? null :
!data.objectCanBeDeleted(id) ?
// TODO: custom msg!
span_title("Object can't be deleted, either there are references to it or you are not authorized", htmlEncode2(unicode_DEL()))
: ahrefWithConfirm(
"Really delete item " + id + "?", deleteLink(id), htmlEncode2(unicode_DEL()), title := "delete");
}
// helpText is also HTML
S addHelpText(S helpText, S html) {
ret empty(helpText) ? html : html + p(small(helpText), style := "text-align: right");
}
S moreSelectizeOptions2(S name) {
ret unnull(moreSelectizeOptions(name))
+ (!haveSelectizeClickable ? "" : [[
, plugins: ['clickable']
, render: {
option: function(item) {
var id = item.text.match(/\d+/)[0];
return '';
}
}
]]);
}
swappable S moreSelectizeOptions(S name) { ret ""; }
// deliver content for dynamic search in combo boxes
S handleComboSearch(SS params) {
S query = params.get("comboSearchQuery");
if (nempty(query)) {
S info = params.get("comboSearchInfo");
ret jsonEncode_shallowLineBreaks(data.comboBoxSearch(info, query));
}
null;
}
// for update or create
swappable SS preprocessUpdateParams(SS params) { ret params; }
void disableAllMutationRights() {
mutationRights = allowCreateOrDelete = allowCreate
= allowEdit = false;
}
swappable bool keepParamInPagination(S name) {
ret eq(name, "search");
}
S customTitleOr(S title) {
ret or2(customTitle, or2(mapGet(params, "title"), title));
}
swappable S selectizeLayoutFix() {
// dirty CSS quick-fix (TODO: send only once)
ret hcss(".selectize-input, .selectize-control { min-width: 300px }");
}
S formExtraHiddens() {
S redirectAfterSave = mapGet(params, "redirectAfterSave");
ret empty(redirectAfterSave) ? "" : hhidden(+redirectAfterSave);
}
void processRenames(SS params) {
if (!allowFieldRenaming) ret;
for (S key1 : keysList(params)) {
S field = dropPrefixOrNull("rename_", key1);
if (field == null) continue;
S newName = trim(params.get(key1));
if (newName == null || eq(field, newName)) continue;
print("Renaming " + field + " to " + or2(newName, ""));
params.remove("rename_" + field);
// Try to catch f_myField, metaInfo_myField, f_myField_1...
S re = "^([^_]+_)" + regexpQuote(field) + "(_[^_]+)?$";
for (S key : keysList(params)) {
LS groups = regexpGroups(re, key);
if (groups != null) {
S newKey = empty(newName) ? null : first(groups) + newName + unnull(second(groups));
mapPut(params, newKey, params.get(key));
params.put(key, ""); // this should delete stuff (if empty strings are converted to null)
print("Renaming key: " + key + " => " + newKey);
}
}
}
}
}