+ const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
+ selector_chosen.className = 'selector-chosen';
+ const title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
+ quickElement(
+ 'span', title_chosen, '',
+ 'class', 'help help-tooltip help-icon',
+ 'title', interpolate(
+ gettext(
+ 'This is the list of chosen %s. You may remove some by ' +
+ 'selecting them in the box below and then clicking the ' +
+ '"Remove" arrow between the two boxes.'
+ ),
+ [field_name]
+ )
+ );
+
+ const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
+ filter_selected_p.className = 'selector-filter';
+
+ const search_filter_selected_label = quickElement('label', filter_selected_p, '', 'for', field_id + '_selected_input');
+
+ quickElement(
+ 'span', search_filter_selected_label, '',
+ 'class', 'help-tooltip search-label-icon',
+ 'title', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
+ );
+
+ filter_selected_p.appendChild(document.createTextNode(' '));
+
+ const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
+ filter_selected_input.id = field_id + '_selected_input';
+
+ const to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', '', 'size', from_box.size, 'name', from_box.name);
+ to_box.className = 'filtered';
+
+ const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
+ quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
+ quickElement('span', warning_footer, ' (click to clear)', 'class', 'list-footer-display__clear');
+
+ const clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link');
+ clear_all.className = 'selector-clearall';
+
+ from_box.name = from_box.name + '_old';
+
+ // Set up the JavaScript event handlers for the select box filter interface
+ const move_selection = function(e, elem, move_func, from, to) {
+ if (elem.classList.contains('active')) {
+ move_func(from, to);
+ SelectFilter.refresh_icons(field_id);
+ SelectFilter.refresh_filtered_selects(field_id);
+ SelectFilter.refresh_filtered_warning(field_id);
+ }
+ e.preventDefault();
+ };
+ choose_all.addEventListener('click', function(e) {
+ move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
+ });
+ add_link.addEventListener('click', function(e) {
+ move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
+ });
+ remove_link.addEventListener('click', function(e) {
+ move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
+ });
+ clear_all.addEventListener('click', function(e) {
+ move_selection(e, this, SelectBox.move_all, field_id + '_to', field_id + '_from');
+ });
+ warning_footer.addEventListener('click', function(e) {
+ filter_selected_input.value = '';
+ SelectBox.filter(field_id + '_to', '');
+ SelectFilter.refresh_filtered_warning(field_id);
+ SelectFilter.refresh_icons(field_id);
+ });
+ filter_input.addEventListener('keypress', function(e) {
+ SelectFilter.filter_key_press(e, field_id, '_from', '_to');
+ });
+ filter_input.addEventListener('keyup', function(e) {
+ SelectFilter.filter_key_up(e, field_id, '_from');
+ });
+ filter_input.addEventListener('keydown', function(e) {
+ SelectFilter.filter_key_down(e, field_id, '_from', '_to');
+ });
+ filter_selected_input.addEventListener('keypress', function(e) {
+ SelectFilter.filter_key_press(e, field_id, '_to', '_from');
+ });
+ filter_selected_input.addEventListener('keyup', function(e) {
+ SelectFilter.filter_key_up(e, field_id, '_to', '_selected_input');
+ });
+ filter_selected_input.addEventListener('keydown', function(e) {
+ SelectFilter.filter_key_down(e, field_id, '_to', '_from');
+ });
+ selector_div.addEventListener('change', function(e) {
+ if (e.target.tagName === 'SELECT') {
+ SelectFilter.refresh_icons(field_id);
+ }
+ });
+ selector_div.addEventListener('dblclick', function(e) {
+ if (e.target.tagName === 'OPTION') {
+ if (e.target.closest('select').id === field_id + '_to') {
+ SelectBox.move(field_id + '_to', field_id + '_from');
+ } else {
+ SelectBox.move(field_id + '_from', field_id + '_to');
+ }
+ SelectFilter.refresh_icons(field_id);
+ }
+ });
+ from_box.closest('form').addEventListener('submit', function() {
+ SelectBox.filter(field_id + '_to', '');
+ SelectBox.select_all(field_id + '_to');
+ });
+ SelectBox.init(field_id + '_from');
+ SelectBox.init(field_id + '_to');
+ // Move selected from_box options to to_box
+ SelectBox.move(field_id + '_from', field_id + '_to');
+
+ // Initial icon refresh
+ SelectFilter.refresh_icons(field_id);
+ },
+ any_selected: function(field) {
+ // Temporarily add the required attribute and check validity.
+ field.required = true;
+ const any_selected = field.checkValidity();
+ field.required = false;
+ return any_selected;
+ },
+ refresh_filtered_warning: function(field_id) {
+ const count = SelectBox.get_hidden_node_count(field_id + '_to');
+ const selector = document.getElementById(field_id + '_selector_chosen');
+ const warning = document.getElementById(field_id + '_list-footer-display-text');
+ selector.className = selector.className.replace('selector-chosen--with-filtered', '');
+ warning.textContent = interpolate(ngettext(
+ '%s selected option not visible',
+ '%s selected options not visible',
+ count
+ ), [count]);
+ if(count > 0) {
+ selector.className += ' selector-chosen--with-filtered';
+ }
+ },
+ refresh_filtered_selects: function(field_id) {
+ SelectBox.filter(field_id + '_from', document.getElementById(field_id + "_input").value);
+ SelectBox.filter(field_id + '_to', document.getElementById(field_id + "_selected_input").value);
+ },
+ refresh_icons: function(field_id) {
+ const from = document.getElementById(field_id + '_from');
+ const to = document.getElementById(field_id + '_to');
+ // Active if at least one item is selected
+ document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from));
+ document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to));
+ // Active if the corresponding box isn't empty
+ document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option'));
+ document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option'));
+ SelectFilter.refresh_filtered_warning(field_id);
+ },
+ filter_key_press: function(event, field_id, source, target) {
+ const source_box = document.getElementById(field_id + source);
+ // don't submit form if user pressed Enter
+ if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) {
+ source_box.selectedIndex = 0;
+ SelectBox.move(field_id + source, field_id + target);
+ source_box.selectedIndex = 0;
+ event.preventDefault();
+ }
+ },
+ filter_key_up: function(event, field_id, source, filter_input) {
+ const input = filter_input || '_input';
+ const source_box = document.getElementById(field_id + source);
+ const temp = source_box.selectedIndex;
+ SelectBox.filter(field_id + source, document.getElementById(field_id + input).value);
+ source_box.selectedIndex = temp;
+ SelectFilter.refresh_filtered_warning(field_id);
+ SelectFilter.refresh_icons(field_id);
+ },
+ filter_key_down: function(event, field_id, source, target) {
+ const source_box = document.getElementById(field_id + source);
+ // right key (39) or left key (37)
+ const direction = source === '_from' ? 39 : 37;
+ // right arrow -- move across
+ if ((event.which && event.which === direction) || (event.keyCode && event.keyCode === direction)) {
+ const old_index = source_box.selectedIndex;
+ SelectBox.move(field_id + source, field_id + target);
+ SelectFilter.refresh_filtered_selects(field_id);
+ SelectFilter.refresh_filtered_warning(field_id);
+ source_box.selectedIndex = (old_index === source_box.length) ? source_box.length - 1 : old_index;
+ return;
+ }
+ // down arrow -- wrap around
+ if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) {
+ source_box.selectedIndex = (source_box.length === source_box.selectedIndex + 1) ? 0 : source_box.selectedIndex + 1;
+ }
+ // up arrow -- wrap around
+ if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) {
+ source_box.selectedIndex = (source_box.selectedIndex === 0) ? source_box.length - 1 : source_box.selectedIndex - 1;
+ }
+ }
+ };
+
+ window.addEventListener('load', function(e) {
+ document.querySelectorAll('select.selectfilter, select.selectfilterstacked').forEach(function(el) {
+ const data = el.dataset;
+ SelectFilter.init(el.id, data.fieldName, parseInt(data.isStacked, 10));
+ });
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/actions.js b/project_manage/staticfiles/admin/js/actions.js
new file mode 100644
index 0000000..6a2ae91
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/actions.js
@@ -0,0 +1,204 @@
+/*global gettext, interpolate, ngettext*/
+'use strict';
+{
+ function show(selector) {
+ document.querySelectorAll(selector).forEach(function(el) {
+ el.classList.remove('hidden');
+ });
+ }
+
+ function hide(selector) {
+ document.querySelectorAll(selector).forEach(function(el) {
+ el.classList.add('hidden');
+ });
+ }
+
+ function showQuestion(options) {
+ hide(options.acrossClears);
+ show(options.acrossQuestions);
+ hide(options.allContainer);
+ }
+
+ function showClear(options) {
+ show(options.acrossClears);
+ hide(options.acrossQuestions);
+ document.querySelector(options.actionContainer).classList.remove(options.selectedClass);
+ show(options.allContainer);
+ hide(options.counterContainer);
+ }
+
+ function reset(options) {
+ hide(options.acrossClears);
+ hide(options.acrossQuestions);
+ hide(options.allContainer);
+ show(options.counterContainer);
+ }
+
+ function clearAcross(options) {
+ reset(options);
+ const acrossInputs = document.querySelectorAll(options.acrossInput);
+ acrossInputs.forEach(function(acrossInput) {
+ acrossInput.value = 0;
+ });
+ document.querySelector(options.actionContainer).classList.remove(options.selectedClass);
+ }
+
+ function checker(actionCheckboxes, options, checked) {
+ if (checked) {
+ showQuestion(options);
+ } else {
+ reset(options);
+ }
+ actionCheckboxes.forEach(function(el) {
+ el.checked = checked;
+ el.closest('tr').classList.toggle(options.selectedClass, checked);
+ });
+ }
+
+ function updateCounter(actionCheckboxes, options) {
+ const sel = Array.from(actionCheckboxes).filter(function(el) {
+ return el.checked;
+ }).length;
+ const counter = document.querySelector(options.counterContainer);
+ // data-actions-icnt is defined in the generated HTML
+ // and contains the total amount of objects in the queryset
+ const actions_icnt = Number(counter.dataset.actionsIcnt);
+ counter.textContent = interpolate(
+ ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
+ sel: sel,
+ cnt: actions_icnt
+ }, true);
+ const allToggle = document.getElementById(options.allToggleId);
+ allToggle.checked = sel === actionCheckboxes.length;
+ if (allToggle.checked) {
+ showQuestion(options);
+ } else {
+ clearAcross(options);
+ }
+ }
+
+ const defaults = {
+ actionContainer: "div.actions",
+ counterContainer: "span.action-counter",
+ allContainer: "div.actions span.all",
+ acrossInput: "div.actions input.select-across",
+ acrossQuestions: "div.actions span.question",
+ acrossClears: "div.actions span.clear",
+ allToggleId: "action-toggle",
+ selectedClass: "selected"
+ };
+
+ window.Actions = function(actionCheckboxes, options) {
+ options = Object.assign({}, defaults, options);
+ let list_editable_changed = false;
+ let lastChecked = null;
+ let shiftPressed = false;
+
+ document.addEventListener('keydown', (event) => {
+ shiftPressed = event.shiftKey;
+ });
+
+ document.addEventListener('keyup', (event) => {
+ shiftPressed = event.shiftKey;
+ });
+
+ document.getElementById(options.allToggleId).addEventListener('click', function(event) {
+ checker(actionCheckboxes, options, this.checked);
+ updateCounter(actionCheckboxes, options);
+ });
+
+ document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) {
+ el.addEventListener('click', function(event) {
+ event.preventDefault();
+ const acrossInputs = document.querySelectorAll(options.acrossInput);
+ acrossInputs.forEach(function(acrossInput) {
+ acrossInput.value = 1;
+ });
+ showClear(options);
+ });
+ });
+
+ document.querySelectorAll(options.acrossClears + " a").forEach(function(el) {
+ el.addEventListener('click', function(event) {
+ event.preventDefault();
+ document.getElementById(options.allToggleId).checked = false;
+ clearAcross(options);
+ checker(actionCheckboxes, options, false);
+ updateCounter(actionCheckboxes, options);
+ });
+ });
+
+ function affectedCheckboxes(target, withModifier) {
+ const multiSelect = (lastChecked && withModifier && lastChecked !== target);
+ if (!multiSelect) {
+ return [target];
+ }
+ const checkboxes = Array.from(actionCheckboxes);
+ const targetIndex = checkboxes.findIndex(el => el === target);
+ const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked);
+ const startIndex = Math.min(targetIndex, lastCheckedIndex);
+ const endIndex = Math.max(targetIndex, lastCheckedIndex);
+ const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex));
+ return filtered;
+ };
+
+ Array.from(document.getElementById('result_list').tBodies).forEach(function(el) {
+ el.addEventListener('change', function(event) {
+ const target = event.target;
+ if (target.classList.contains('action-select')) {
+ const checkboxes = affectedCheckboxes(target, shiftPressed);
+ checker(checkboxes, options, target.checked);
+ updateCounter(actionCheckboxes, options);
+ lastChecked = target;
+ } else {
+ list_editable_changed = true;
+ }
+ });
+ });
+
+ document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) {
+ if (list_editable_changed) {
+ const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
+ if (!confirmed) {
+ event.preventDefault();
+ }
+ }
+ });
+
+ const el = document.querySelector('#changelist-form input[name=_save]');
+ // The button does not exist if no fields are editable.
+ if (el) {
+ el.addEventListener('click', function(event) {
+ if (document.querySelector('[name=action]').value) {
+ const text = list_editable_changed
+ ? gettext("You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action.")
+ : gettext("You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Go button rather than the Save button.");
+ if (!confirm(text)) {
+ event.preventDefault();
+ }
+ }
+ });
+ }
+ // Sync counter when navigating to the page, such as through the back
+ // button.
+ window.addEventListener('pageshow', (event) => updateCounter(actionCheckboxes, options));
+ };
+
+ // Call function fn when the DOM is loaded and ready. If it is already
+ // loaded, call the function now.
+ // http://youmightnotneedjquery.com/#ready
+ function ready(fn) {
+ if (document.readyState !== 'loading') {
+ fn();
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+ }
+
+ ready(function() {
+ const actionsEls = document.querySelectorAll('tr input.action-select');
+ if (actionsEls.length > 0) {
+ Actions(actionsEls);
+ }
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/admin/DateTimeShortcuts.js b/project_manage/staticfiles/admin/js/admin/DateTimeShortcuts.js
new file mode 100644
index 0000000..aa1cae9
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/admin/DateTimeShortcuts.js
@@ -0,0 +1,408 @@
+/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/
+// Inserts shortcut buttons after all of the following:
+//
+//
+'use strict';
+{
+ const DateTimeShortcuts = {
+ calendars: [],
+ calendarInputs: [],
+ clockInputs: [],
+ clockHours: {
+ default_: [
+ [gettext_noop('Now'), -1],
+ [gettext_noop('Midnight'), 0],
+ [gettext_noop('6 a.m.'), 6],
+ [gettext_noop('Noon'), 12],
+ [gettext_noop('6 p.m.'), 18]
+ ]
+ },
+ dismissClockFunc: [],
+ dismissCalendarFunc: [],
+ calendarDivName1: 'calendarbox', // name of calendar
that gets toggled
+ calendarDivName2: 'calendarin', // name of
that contains calendar
+ calendarLinkName: 'calendarlink', // name of the link that is used to toggle
+ clockDivName: 'clockbox', // name of clock
that gets toggled
+ clockLinkName: 'clocklink', // name of the link that is used to toggle
+ shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts
+ timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch
+ timezoneOffset: 0,
+ init: function() {
+ const serverOffset = document.body.dataset.adminUtcOffset;
+ if (serverOffset) {
+ const localOffset = new Date().getTimezoneOffset() * -60;
+ DateTimeShortcuts.timezoneOffset = localOffset - serverOffset;
+ }
+
+ for (const inp of document.getElementsByTagName('input')) {
+ if (inp.type === 'text' && inp.classList.contains('vTimeField')) {
+ DateTimeShortcuts.addClock(inp);
+ DateTimeShortcuts.addTimezoneWarning(inp);
+ }
+ else if (inp.type === 'text' && inp.classList.contains('vDateField')) {
+ DateTimeShortcuts.addCalendar(inp);
+ DateTimeShortcuts.addTimezoneWarning(inp);
+ }
+ }
+ },
+ // Return the current time while accounting for the server timezone.
+ now: function() {
+ const serverOffset = document.body.dataset.adminUtcOffset;
+ if (serverOffset) {
+ const localNow = new Date();
+ const localOffset = localNow.getTimezoneOffset() * -60;
+ localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset));
+ return localNow;
+ } else {
+ return new Date();
+ }
+ },
+ // Add a warning when the time zone in the browser and backend do not match.
+ addTimezoneWarning: function(inp) {
+ const warningClass = DateTimeShortcuts.timezoneWarningClass;
+ let timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600;
+
+ // Only warn if there is a time zone mismatch.
+ if (!timezoneOffset) {
+ return;
+ }
+
+ // Check if warning is already there.
+ if (inp.parentNode.querySelectorAll('.' + warningClass).length) {
+ return;
+ }
+
+ let message;
+ if (timezoneOffset > 0) {
+ message = ngettext(
+ 'Note: You are %s hour ahead of server time.',
+ 'Note: You are %s hours ahead of server time.',
+ timezoneOffset
+ );
+ }
+ else {
+ timezoneOffset *= -1;
+ message = ngettext(
+ 'Note: You are %s hour behind server time.',
+ 'Note: You are %s hours behind server time.',
+ timezoneOffset
+ );
+ }
+ message = interpolate(message, [timezoneOffset]);
+
+ const warning = document.createElement('div');
+ warning.classList.add('help', warningClass);
+ warning.textContent = message;
+ inp.parentNode.appendChild(warning);
+ },
+ // Add clock widget to a given field
+ addClock: function(inp) {
+ const num = DateTimeShortcuts.clockInputs.length;
+ DateTimeShortcuts.clockInputs[num] = inp;
+ DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; };
+
+ // Shortcut links (clock icon and "Now" link)
+ const shortcuts_span = document.createElement('span');
+ shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
+ inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
+ const now_link = document.createElement('a');
+ now_link.href = "#";
+ now_link.textContent = gettext('Now');
+ now_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, -1);
+ });
+ const clock_link = document.createElement('a');
+ clock_link.href = '#';
+ clock_link.id = DateTimeShortcuts.clockLinkName + num;
+ clock_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ // avoid triggering the document click handler to dismiss the clock
+ e.stopPropagation();
+ DateTimeShortcuts.openClock(num);
+ });
+
+ quickElement(
+ 'span', clock_link, '',
+ 'class', 'clock-icon',
+ 'title', gettext('Choose a Time')
+ );
+ shortcuts_span.appendChild(document.createTextNode('\u00A0'));
+ shortcuts_span.appendChild(now_link);
+ shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
+ shortcuts_span.appendChild(clock_link);
+
+ // Create clock link div
+ //
+ // Markup looks like:
+ //
+ //
Choose a time
+ //
+ //
Cancel
+ //
+
+ const clock_box = document.createElement('div');
+ clock_box.style.display = 'none';
+ clock_box.style.position = 'absolute';
+ clock_box.className = 'clockbox module';
+ clock_box.id = DateTimeShortcuts.clockDivName + num;
+ document.body.appendChild(clock_box);
+ clock_box.addEventListener('click', function(e) { e.stopPropagation(); });
+
+ quickElement('h2', clock_box, gettext('Choose a time'));
+ const time_list = quickElement('ul', clock_box);
+ time_list.className = 'timelist';
+ // The list of choices can be overridden in JavaScript like this:
+ // DateTimeShortcuts.clockHours.name = [['3 a.m.', 3]];
+ // where name is the name attribute of the
.
+ const name = typeof DateTimeShortcuts.clockHours[inp.name] === 'undefined' ? 'default_' : inp.name;
+ DateTimeShortcuts.clockHours[name].forEach(function(element) {
+ const time_link = quickElement('a', quickElement('li', time_list), gettext(element[0]), 'href', '#');
+ time_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, element[1]);
+ });
+ });
+
+ const cancel_p = quickElement('p', clock_box);
+ cancel_p.className = 'calendar-cancel';
+ const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
+ cancel_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.dismissClock(num);
+ });
+
+ document.addEventListener('keyup', function(event) {
+ if (event.which === 27) {
+ // ESC key closes popup
+ DateTimeShortcuts.dismissClock(num);
+ event.preventDefault();
+ }
+ });
+ },
+ openClock: function(num) {
+ const clock_box = document.getElementById(DateTimeShortcuts.clockDivName + num);
+ const clock_link = document.getElementById(DateTimeShortcuts.clockLinkName + num);
+
+ // Recalculate the clockbox position
+ // is it left-to-right or right-to-left layout ?
+ if (window.getComputedStyle(document.body).direction !== 'rtl') {
+ clock_box.style.left = findPosX(clock_link) + 17 + 'px';
+ }
+ else {
+ // since style's width is in em, it'd be tough to calculate
+ // px value of it. let's use an estimated px for now
+ clock_box.style.left = findPosX(clock_link) - 110 + 'px';
+ }
+ clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px';
+
+ // Show the clock box
+ clock_box.style.display = 'block';
+ document.addEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
+ },
+ dismissClock: function(num) {
+ document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none';
+ document.removeEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
+ },
+ handleClockQuicklink: function(num, val) {
+ let d;
+ if (val === -1) {
+ d = DateTimeShortcuts.now();
+ }
+ else {
+ d = new Date(1970, 1, 1, val, 0, 0, 0);
+ }
+ DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]);
+ DateTimeShortcuts.clockInputs[num].focus();
+ DateTimeShortcuts.dismissClock(num);
+ },
+ // Add calendar widget to a given field.
+ addCalendar: function(inp) {
+ const num = DateTimeShortcuts.calendars.length;
+
+ DateTimeShortcuts.calendarInputs[num] = inp;
+ DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; };
+
+ // Shortcut links (calendar icon and "Today" link)
+ const shortcuts_span = document.createElement('span');
+ shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
+ inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
+ const today_link = document.createElement('a');
+ today_link.href = '#';
+ today_link.appendChild(document.createTextNode(gettext('Today')));
+ today_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, 0);
+ });
+ const cal_link = document.createElement('a');
+ cal_link.href = '#';
+ cal_link.id = DateTimeShortcuts.calendarLinkName + num;
+ cal_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ // avoid triggering the document click handler to dismiss the calendar
+ e.stopPropagation();
+ DateTimeShortcuts.openCalendar(num);
+ });
+ quickElement(
+ 'span', cal_link, '',
+ 'class', 'date-icon',
+ 'title', gettext('Choose a Date')
+ );
+ shortcuts_span.appendChild(document.createTextNode('\u00A0'));
+ shortcuts_span.appendChild(today_link);
+ shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
+ shortcuts_span.appendChild(cal_link);
+
+ // Create calendarbox div.
+ //
+ // Markup looks like:
+ //
+ //
+ //
+ // ‹
+ // › February 2003
+ //
+ //
+ //
+ //
+ //
+ //
Cancel
+ //
+ const cal_box = document.createElement('div');
+ cal_box.style.display = 'none';
+ cal_box.style.position = 'absolute';
+ cal_box.className = 'calendarbox module';
+ cal_box.id = DateTimeShortcuts.calendarDivName1 + num;
+ document.body.appendChild(cal_box);
+ cal_box.addEventListener('click', function(e) { e.stopPropagation(); });
+
+ // next-prev links
+ const cal_nav = quickElement('div', cal_box);
+ const cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#');
+ cal_nav_prev.className = 'calendarnav-previous';
+ cal_nav_prev.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.drawPrev(num);
+ });
+
+ const cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#');
+ cal_nav_next.className = 'calendarnav-next';
+ cal_nav_next.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.drawNext(num);
+ });
+
+ // main box
+ const cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
+ cal_main.className = 'calendar';
+ DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num));
+ DateTimeShortcuts.calendars[num].drawCurrent();
+
+ // calendar shortcuts
+ const shortcuts = quickElement('div', cal_box);
+ shortcuts.className = 'calendar-shortcuts';
+ let day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'href', '#');
+ day_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, -1);
+ });
+ shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
+ day_link = quickElement('a', shortcuts, gettext('Today'), 'href', '#');
+ day_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, 0);
+ });
+ shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
+ day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'href', '#');
+ day_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, +1);
+ });
+
+ // cancel bar
+ const cancel_p = quickElement('p', cal_box);
+ cancel_p.className = 'calendar-cancel';
+ const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
+ cancel_link.addEventListener('click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.dismissCalendar(num);
+ });
+ document.addEventListener('keyup', function(event) {
+ if (event.which === 27) {
+ // ESC key closes popup
+ DateTimeShortcuts.dismissCalendar(num);
+ event.preventDefault();
+ }
+ });
+ },
+ openCalendar: function(num) {
+ const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num);
+ const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num);
+ const inp = DateTimeShortcuts.calendarInputs[num];
+
+ // Determine if the current value in the input has a valid date.
+ // If so, draw the calendar with that date's year and month.
+ if (inp.value) {
+ const format = get_format('DATE_INPUT_FORMATS')[0];
+ const selected = inp.value.strptime(format);
+ const year = selected.getUTCFullYear();
+ const month = selected.getUTCMonth() + 1;
+ const re = /\d{4}/;
+ if (re.test(year.toString()) && month >= 1 && month <= 12) {
+ DateTimeShortcuts.calendars[num].drawDate(month, year, selected);
+ }
+ }
+
+ // Recalculate the clockbox position
+ // is it left-to-right or right-to-left layout ?
+ if (window.getComputedStyle(document.body).direction !== 'rtl') {
+ cal_box.style.left = findPosX(cal_link) + 17 + 'px';
+ }
+ else {
+ // since style's width is in em, it'd be tough to calculate
+ // px value of it. let's use an estimated px for now
+ cal_box.style.left = findPosX(cal_link) - 180 + 'px';
+ }
+ cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px';
+
+ cal_box.style.display = 'block';
+ document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
+ },
+ dismissCalendar: function(num) {
+ document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
+ document.removeEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
+ },
+ drawPrev: function(num) {
+ DateTimeShortcuts.calendars[num].drawPreviousMonth();
+ },
+ drawNext: function(num) {
+ DateTimeShortcuts.calendars[num].drawNextMonth();
+ },
+ handleCalendarCallback: function(num) {
+ const format = get_format('DATE_INPUT_FORMATS')[0];
+ return function(y, m, d) {
+ DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format);
+ DateTimeShortcuts.calendarInputs[num].focus();
+ document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
+ };
+ },
+ handleCalendarQuickLink: function(num, offset) {
+ const d = DateTimeShortcuts.now();
+ d.setDate(d.getDate() + offset);
+ DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]);
+ DateTimeShortcuts.calendarInputs[num].focus();
+ DateTimeShortcuts.dismissCalendar(num);
+ }
+ };
+
+ window.addEventListener('load', DateTimeShortcuts.init);
+ window.DateTimeShortcuts = DateTimeShortcuts;
+}
diff --git a/project_manage/staticfiles/admin/js/admin/RelatedObjectLookups.js b/project_manage/staticfiles/admin/js/admin/RelatedObjectLookups.js
new file mode 100644
index 0000000..32e3f5b
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/admin/RelatedObjectLookups.js
@@ -0,0 +1,240 @@
+/*global SelectBox, interpolate*/
+// Handles related-objects functionality: lookup link for raw_id_fields
+// and Add Another links.
+'use strict';
+{
+ const $ = django.jQuery;
+ let popupIndex = 0;
+ const relatedWindows = [];
+
+ function dismissChildPopups() {
+ relatedWindows.forEach(function(win) {
+ if(!win.closed) {
+ win.dismissChildPopups();
+ win.close();
+ }
+ });
+ }
+
+ function setPopupIndex() {
+ if(document.getElementsByName("_popup").length > 0) {
+ const index = window.name.lastIndexOf("__") + 2;
+ popupIndex = parseInt(window.name.substring(index));
+ } else {
+ popupIndex = 0;
+ }
+ }
+
+ function addPopupIndex(name) {
+ return name + "__" + (popupIndex + 1);
+ }
+
+ function removePopupIndex(name) {
+ return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), '');
+ }
+
+ function showAdminPopup(triggeringLink, name_regexp, add_popup) {
+ const name = addPopupIndex(triggeringLink.id.replace(name_regexp, ''));
+ const href = new URL(triggeringLink.href);
+ if (add_popup) {
+ href.searchParams.set('_popup', 1);
+ }
+ const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
+ relatedWindows.push(win);
+ win.focus();
+ return false;
+ }
+
+ function showRelatedObjectLookupPopup(triggeringLink) {
+ return showAdminPopup(triggeringLink, /^lookup_/, true);
+ }
+
+ function dismissRelatedLookupPopup(win, chosenId) {
+ const name = removePopupIndex(win.name);
+ const elem = document.getElementById(name);
+ if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
+ elem.value += ',' + chosenId;
+ } else {
+ document.getElementById(name).value = chosenId;
+ }
+ const index = relatedWindows.indexOf(win);
+ if (index > -1) {
+ relatedWindows.splice(index, 1);
+ }
+ win.close();
+ }
+
+ function showRelatedObjectPopup(triggeringLink) {
+ return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false);
+ }
+
+ function updateRelatedObjectLinks(triggeringLink) {
+ const $this = $(triggeringLink);
+ const siblings = $this.nextAll('.view-related, .change-related, .delete-related');
+ if (!siblings.length) {
+ return;
+ }
+ const value = $this.val();
+ if (value) {
+ siblings.each(function() {
+ const elm = $(this);
+ elm.attr('href', elm.attr('data-href-template').replace('__fk__', value));
+ elm.removeAttr('aria-disabled');
+ });
+ } else {
+ siblings.removeAttr('href');
+ siblings.attr('aria-disabled', true);
+ }
+ }
+
+ function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId) {
+ // After create/edit a model from the options next to the current
+ // select (+ or :pencil:) update ForeignKey PK of the rest of selects
+ // in the page.
+
+ const path = win.location.pathname;
+ // Extract the model from the popup url '.../
/add/' or
+ // '...///change/' depending the action (add or change).
+ const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)];
+ // Exclude autocomplete selects.
+ const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] select:not(.admin-autocomplete)`);
+
+ selectsRelated.forEach(function(select) {
+ if (currentSelect === select) {
+ return;
+ }
+
+ let option = select.querySelector(`option[value="${objId}"]`);
+
+ if (!option) {
+ option = new Option(newRepr, newId);
+ select.options.add(option);
+ return;
+ }
+
+ option.textContent = newRepr;
+ option.value = newId;
+ });
+ }
+
+ function dismissAddRelatedObjectPopup(win, newId, newRepr) {
+ const name = removePopupIndex(win.name);
+ const elem = document.getElementById(name);
+ if (elem) {
+ const elemName = elem.nodeName.toUpperCase();
+ if (elemName === 'SELECT') {
+ elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
+ updateRelatedSelectsOptions(elem, win, null, newRepr, newId);
+ } else if (elemName === 'INPUT') {
+ if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
+ elem.value += ',' + newId;
+ } else {
+ elem.value = newId;
+ }
+ }
+ // Trigger a change event to update related links if required.
+ $(elem).trigger('change');
+ } else {
+ const toId = name + "_to";
+ const o = new Option(newRepr, newId);
+ SelectBox.add_to_cache(toId, o);
+ SelectBox.redisplay(toId);
+ }
+ const index = relatedWindows.indexOf(win);
+ if (index > -1) {
+ relatedWindows.splice(index, 1);
+ }
+ win.close();
+ }
+
+ function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
+ const id = removePopupIndex(win.name.replace(/^edit_/, ''));
+ const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
+ const selects = $(selectsSelector);
+ selects.find('option').each(function() {
+ if (this.value === objId) {
+ this.textContent = newRepr;
+ this.value = newId;
+ }
+ }).trigger('change');
+ updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId);
+ selects.next().find('.select2-selection__rendered').each(function() {
+ // The element can have a clear button as a child.
+ // Use the lastChild to modify only the displayed value.
+ this.lastChild.textContent = newRepr;
+ this.title = newRepr;
+ });
+ const index = relatedWindows.indexOf(win);
+ if (index > -1) {
+ relatedWindows.splice(index, 1);
+ }
+ win.close();
+ }
+
+ function dismissDeleteRelatedObjectPopup(win, objId) {
+ const id = removePopupIndex(win.name.replace(/^delete_/, ''));
+ const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
+ const selects = $(selectsSelector);
+ selects.find('option').each(function() {
+ if (this.value === objId) {
+ $(this).remove();
+ }
+ }).trigger('change');
+ const index = relatedWindows.indexOf(win);
+ if (index > -1) {
+ relatedWindows.splice(index, 1);
+ }
+ win.close();
+ }
+
+ window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup;
+ window.dismissRelatedLookupPopup = dismissRelatedLookupPopup;
+ window.showRelatedObjectPopup = showRelatedObjectPopup;
+ window.updateRelatedObjectLinks = updateRelatedObjectLinks;
+ window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
+ window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
+ window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
+ window.dismissChildPopups = dismissChildPopups;
+
+ // Kept for backward compatibility
+ window.showAddAnotherPopup = showRelatedObjectPopup;
+ window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
+
+ window.addEventListener('unload', function(evt) {
+ window.dismissChildPopups();
+ });
+
+ $(document).ready(function() {
+ setPopupIndex();
+ $("a[data-popup-opener]").on('click', function(event) {
+ event.preventDefault();
+ opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
+ });
+ $('body').on('click', '.related-widget-wrapper-link[data-popup="yes"]', function(e) {
+ e.preventDefault();
+ if (this.href) {
+ const event = $.Event('django:show-related', {href: this.href});
+ $(this).trigger(event);
+ if (!event.isDefaultPrevented()) {
+ showRelatedObjectPopup(this);
+ }
+ }
+ });
+ $('body').on('change', '.related-widget-wrapper select', function(e) {
+ const event = $.Event('django:update-related');
+ $(this).trigger(event);
+ if (!event.isDefaultPrevented()) {
+ updateRelatedObjectLinks(this);
+ }
+ });
+ $('.related-widget-wrapper select').trigger('change');
+ $('body').on('click', '.related-lookup', function(e) {
+ e.preventDefault();
+ const event = $.Event('django:lookup-related');
+ $(this).trigger(event);
+ if (!event.isDefaultPrevented()) {
+ showRelatedObjectLookupPopup(this);
+ }
+ });
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/autocomplete.js b/project_manage/staticfiles/admin/js/autocomplete.js
new file mode 100644
index 0000000..d3daeab
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/autocomplete.js
@@ -0,0 +1,33 @@
+'use strict';
+{
+ const $ = django.jQuery;
+
+ $.fn.djangoAdminSelect2 = function() {
+ $.each(this, function(i, element) {
+ $(element).select2({
+ ajax: {
+ data: (params) => {
+ return {
+ term: params.term,
+ page: params.page,
+ app_label: element.dataset.appLabel,
+ model_name: element.dataset.modelName,
+ field_name: element.dataset.fieldName
+ };
+ }
+ }
+ });
+ });
+ return this;
+ };
+
+ $(function() {
+ // Initialize all autocomplete widgets except the one in the template
+ // form used when a new formset is added.
+ $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2();
+ });
+
+ document.addEventListener('formset:added', (event) => {
+ $(event.target).find('.admin-autocomplete').djangoAdminSelect2();
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/calendar.js b/project_manage/staticfiles/admin/js/calendar.js
new file mode 100644
index 0000000..776310f
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/calendar.js
@@ -0,0 +1,239 @@
+/*global gettext, pgettext, get_format, quickElement, removeChildren*/
+/*
+calendar.js - Calendar functions by Adrian Holovaty
+depends on core.js for utility functions like removeChildren or quickElement
+*/
+'use strict';
+{
+ // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions
+ const CalendarNamespace = {
+ monthsOfYear: [
+ gettext('January'),
+ gettext('February'),
+ gettext('March'),
+ gettext('April'),
+ gettext('May'),
+ gettext('June'),
+ gettext('July'),
+ gettext('August'),
+ gettext('September'),
+ gettext('October'),
+ gettext('November'),
+ gettext('December')
+ ],
+ monthsOfYearAbbrev: [
+ pgettext('abbrev. month January', 'Jan'),
+ pgettext('abbrev. month February', 'Feb'),
+ pgettext('abbrev. month March', 'Mar'),
+ pgettext('abbrev. month April', 'Apr'),
+ pgettext('abbrev. month May', 'May'),
+ pgettext('abbrev. month June', 'Jun'),
+ pgettext('abbrev. month July', 'Jul'),
+ pgettext('abbrev. month August', 'Aug'),
+ pgettext('abbrev. month September', 'Sep'),
+ pgettext('abbrev. month October', 'Oct'),
+ pgettext('abbrev. month November', 'Nov'),
+ pgettext('abbrev. month December', 'Dec')
+ ],
+ daysOfWeek: [
+ gettext('Sunday'),
+ gettext('Monday'),
+ gettext('Tuesday'),
+ gettext('Wednesday'),
+ gettext('Thursday'),
+ gettext('Friday'),
+ gettext('Saturday')
+ ],
+ daysOfWeekAbbrev: [
+ pgettext('abbrev. day Sunday', 'Sun'),
+ pgettext('abbrev. day Monday', 'Mon'),
+ pgettext('abbrev. day Tuesday', 'Tue'),
+ pgettext('abbrev. day Wednesday', 'Wed'),
+ pgettext('abbrev. day Thursday', 'Thur'),
+ pgettext('abbrev. day Friday', 'Fri'),
+ pgettext('abbrev. day Saturday', 'Sat')
+ ],
+ daysOfWeekInitial: [
+ pgettext('one letter Sunday', 'S'),
+ pgettext('one letter Monday', 'M'),
+ pgettext('one letter Tuesday', 'T'),
+ pgettext('one letter Wednesday', 'W'),
+ pgettext('one letter Thursday', 'T'),
+ pgettext('one letter Friday', 'F'),
+ pgettext('one letter Saturday', 'S')
+ ],
+ firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')),
+ isLeapYear: function(year) {
+ return (((year % 4) === 0) && ((year % 100) !== 0 ) || ((year % 400) === 0));
+ },
+ getDaysInMonth: function(month, year) {
+ let days;
+ if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) {
+ days = 31;
+ }
+ else if (month === 4 || month === 6 || month === 9 || month === 11) {
+ days = 30;
+ }
+ else if (month === 2 && CalendarNamespace.isLeapYear(year)) {
+ days = 29;
+ }
+ else {
+ days = 28;
+ }
+ return days;
+ },
+ draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999
+ const today = new Date();
+ const todayDay = today.getDate();
+ const todayMonth = today.getMonth() + 1;
+ const todayYear = today.getFullYear();
+ let todayClass = '';
+
+ // Use UTC functions here because the date field does not contain time
+ // and using the UTC function variants prevent the local time offset
+ // from altering the date, specifically the day field. For example:
+ //
+ // ```
+ // var x = new Date('2013-10-02');
+ // var day = x.getDate();
+ // ```
+ //
+ // The day variable above will be 1 instead of 2 in, say, US Pacific time
+ // zone.
+ let isSelectedMonth = false;
+ if (typeof selected !== 'undefined') {
+ isSelectedMonth = (selected.getUTCFullYear() === year && (selected.getUTCMonth() + 1) === month);
+ }
+
+ month = parseInt(month);
+ year = parseInt(year);
+ const calDiv = document.getElementById(div_id);
+ removeChildren(calDiv);
+ const calTable = document.createElement('table');
+ quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month - 1] + ' ' + year);
+ const tableBody = quickElement('tbody', calTable);
+
+ // Draw days-of-week header
+ let tableRow = quickElement('tr', tableBody);
+ for (let i = 0; i < 7; i++) {
+ quickElement('th', tableRow, CalendarNamespace.daysOfWeekInitial[(i + CalendarNamespace.firstDayOfWeek) % 7]);
+ }
+
+ const startingPos = new Date(year, month - 1, 1 - CalendarNamespace.firstDayOfWeek).getDay();
+ const days = CalendarNamespace.getDaysInMonth(month, year);
+
+ let nonDayCell;
+
+ // Draw blanks before first of month
+ tableRow = quickElement('tr', tableBody);
+ for (let i = 0; i < startingPos; i++) {
+ nonDayCell = quickElement('td', tableRow, ' ');
+ nonDayCell.className = "nonday";
+ }
+
+ function calendarMonth(y, m) {
+ function onClick(e) {
+ e.preventDefault();
+ callback(y, m, this.textContent);
+ }
+ return onClick;
+ }
+
+ // Draw days of month
+ let currentDay = 1;
+ for (let i = startingPos; currentDay <= days; i++) {
+ if (i % 7 === 0 && currentDay !== 1) {
+ tableRow = quickElement('tr', tableBody);
+ }
+ if ((currentDay === todayDay) && (month === todayMonth) && (year === todayYear)) {
+ todayClass = 'today';
+ } else {
+ todayClass = '';
+ }
+
+ // use UTC function; see above for explanation.
+ if (isSelectedMonth && currentDay === selected.getUTCDate()) {
+ if (todayClass !== '') {
+ todayClass += " ";
+ }
+ todayClass += "selected";
+ }
+
+ const cell = quickElement('td', tableRow, '', 'class', todayClass);
+ const link = quickElement('a', cell, currentDay, 'href', '#');
+ link.addEventListener('click', calendarMonth(year, month));
+ currentDay++;
+ }
+
+ // Draw blanks after end of month (optional, but makes for valid code)
+ while (tableRow.childNodes.length < 7) {
+ nonDayCell = quickElement('td', tableRow, ' ');
+ nonDayCell.className = "nonday";
+ }
+
+ calDiv.appendChild(calTable);
+ }
+ };
+
+ // Calendar -- A calendar instance
+ function Calendar(div_id, callback, selected) {
+ // div_id (string) is the ID of the element in which the calendar will
+ // be displayed
+ // callback (string) is the name of a JavaScript function that will be
+ // called with the parameters (year, month, day) when a day in the
+ // calendar is clicked
+ this.div_id = div_id;
+ this.callback = callback;
+ this.today = new Date();
+ this.currentMonth = this.today.getMonth() + 1;
+ this.currentYear = this.today.getFullYear();
+ if (typeof selected !== 'undefined') {
+ this.selected = selected;
+ }
+ }
+ Calendar.prototype = {
+ drawCurrent: function() {
+ CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected);
+ },
+ drawDate: function(month, year, selected) {
+ this.currentMonth = month;
+ this.currentYear = year;
+
+ if(selected) {
+ this.selected = selected;
+ }
+
+ this.drawCurrent();
+ },
+ drawPreviousMonth: function() {
+ if (this.currentMonth === 1) {
+ this.currentMonth = 12;
+ this.currentYear--;
+ }
+ else {
+ this.currentMonth--;
+ }
+ this.drawCurrent();
+ },
+ drawNextMonth: function() {
+ if (this.currentMonth === 12) {
+ this.currentMonth = 1;
+ this.currentYear++;
+ }
+ else {
+ this.currentMonth++;
+ }
+ this.drawCurrent();
+ },
+ drawPreviousYear: function() {
+ this.currentYear--;
+ this.drawCurrent();
+ },
+ drawNextYear: function() {
+ this.currentYear++;
+ this.drawCurrent();
+ }
+ };
+ window.Calendar = Calendar;
+ window.CalendarNamespace = CalendarNamespace;
+}
diff --git a/project_manage/staticfiles/admin/js/cancel.js b/project_manage/staticfiles/admin/js/cancel.js
new file mode 100644
index 0000000..3069c6f
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/cancel.js
@@ -0,0 +1,29 @@
+'use strict';
+{
+ // Call function fn when the DOM is loaded and ready. If it is already
+ // loaded, call the function now.
+ // http://youmightnotneedjquery.com/#ready
+ function ready(fn) {
+ if (document.readyState !== 'loading') {
+ fn();
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+ }
+
+ ready(function() {
+ function handleClick(event) {
+ event.preventDefault();
+ const params = new URLSearchParams(window.location.search);
+ if (params.has('_popup')) {
+ window.close(); // Close the popup.
+ } else {
+ window.history.back(); // Otherwise, go back.
+ }
+ }
+
+ document.querySelectorAll('.cancel-link').forEach(function(el) {
+ el.addEventListener('click', handleClick);
+ });
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/change_form.js b/project_manage/staticfiles/admin/js/change_form.js
new file mode 100644
index 0000000..96a4c62
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/change_form.js
@@ -0,0 +1,16 @@
+'use strict';
+{
+ const inputTags = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
+ const modelName = document.getElementById('django-admin-form-add-constants').dataset.modelName;
+ if (modelName) {
+ const form = document.getElementById(modelName + '_form');
+ for (const element of form.elements) {
+ // HTMLElement.offsetParent returns null when the element is not
+ // rendered.
+ if (inputTags.includes(element.tagName) && !element.disabled && element.offsetParent) {
+ element.focus();
+ break;
+ }
+ }
+ }
+}
diff --git a/project_manage/staticfiles/admin/js/collapse.js b/project_manage/staticfiles/admin/js/collapse.js
new file mode 100644
index 0000000..c6c7b0f
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/collapse.js
@@ -0,0 +1,43 @@
+/*global gettext*/
+'use strict';
+{
+ window.addEventListener('load', function() {
+ // Add anchor tag for Show/Hide link
+ const fieldsets = document.querySelectorAll('fieldset.collapse');
+ for (const [i, elem] of fieldsets.entries()) {
+ // Don't hide if fields in this fieldset have errors
+ if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
+ elem.classList.add('collapsed');
+ const h2 = elem.querySelector('h2');
+ const link = document.createElement('a');
+ link.id = 'fieldsetcollapser' + i;
+ link.className = 'collapse-toggle';
+ link.href = '#';
+ link.textContent = gettext('Show');
+ h2.appendChild(document.createTextNode(' ('));
+ h2.appendChild(link);
+ h2.appendChild(document.createTextNode(')'));
+ }
+ }
+ // Add toggle to hide/show anchor tag
+ const toggleFunc = function(ev) {
+ if (ev.target.matches('.collapse-toggle')) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ const fieldset = ev.target.closest('fieldset');
+ if (fieldset.classList.contains('collapsed')) {
+ // Show
+ ev.target.textContent = gettext('Hide');
+ fieldset.classList.remove('collapsed');
+ } else {
+ // Hide
+ ev.target.textContent = gettext('Show');
+ fieldset.classList.add('collapsed');
+ }
+ }
+ };
+ document.querySelectorAll('fieldset.module').forEach(function(el) {
+ el.addEventListener('click', toggleFunc);
+ });
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/core.js b/project_manage/staticfiles/admin/js/core.js
new file mode 100644
index 0000000..10504d4
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/core.js
@@ -0,0 +1,184 @@
+// Core JavaScript helper functions
+'use strict';
+
+// quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]);
+function quickElement() {
+ const obj = document.createElement(arguments[0]);
+ if (arguments[2]) {
+ const textNode = document.createTextNode(arguments[2]);
+ obj.appendChild(textNode);
+ }
+ const len = arguments.length;
+ for (let i = 3; i < len; i += 2) {
+ obj.setAttribute(arguments[i], arguments[i + 1]);
+ }
+ arguments[1].appendChild(obj);
+ return obj;
+}
+
+// "a" is reference to an object
+function removeChildren(a) {
+ while (a.hasChildNodes()) {
+ a.removeChild(a.lastChild);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Find-position functions by PPK
+// See https://www.quirksmode.org/js/findpos.html
+// ----------------------------------------------------------------------------
+function findPosX(obj) {
+ let curleft = 0;
+ if (obj.offsetParent) {
+ while (obj.offsetParent) {
+ curleft += obj.offsetLeft - obj.scrollLeft;
+ obj = obj.offsetParent;
+ }
+ } else if (obj.x) {
+ curleft += obj.x;
+ }
+ return curleft;
+}
+
+function findPosY(obj) {
+ let curtop = 0;
+ if (obj.offsetParent) {
+ while (obj.offsetParent) {
+ curtop += obj.offsetTop - obj.scrollTop;
+ obj = obj.offsetParent;
+ }
+ } else if (obj.y) {
+ curtop += obj.y;
+ }
+ return curtop;
+}
+
+//-----------------------------------------------------------------------------
+// Date object extensions
+// ----------------------------------------------------------------------------
+{
+ Date.prototype.getTwelveHours = function() {
+ return this.getHours() % 12 || 12;
+ };
+
+ Date.prototype.getTwoDigitMonth = function() {
+ return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1);
+ };
+
+ Date.prototype.getTwoDigitDate = function() {
+ return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate();
+ };
+
+ Date.prototype.getTwoDigitTwelveHour = function() {
+ return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours();
+ };
+
+ Date.prototype.getTwoDigitHour = function() {
+ return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours();
+ };
+
+ Date.prototype.getTwoDigitMinute = function() {
+ return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes();
+ };
+
+ Date.prototype.getTwoDigitSecond = function() {
+ return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds();
+ };
+
+ Date.prototype.getAbbrevDayName = function() {
+ return typeof window.CalendarNamespace === "undefined"
+ ? '0' + this.getDay()
+ : window.CalendarNamespace.daysOfWeekAbbrev[this.getDay()];
+ };
+
+ Date.prototype.getFullDayName = function() {
+ return typeof window.CalendarNamespace === "undefined"
+ ? '0' + this.getDay()
+ : window.CalendarNamespace.daysOfWeek[this.getDay()];
+ };
+
+ Date.prototype.getAbbrevMonthName = function() {
+ return typeof window.CalendarNamespace === "undefined"
+ ? this.getTwoDigitMonth()
+ : window.CalendarNamespace.monthsOfYearAbbrev[this.getMonth()];
+ };
+
+ Date.prototype.getFullMonthName = function() {
+ return typeof window.CalendarNamespace === "undefined"
+ ? this.getTwoDigitMonth()
+ : window.CalendarNamespace.monthsOfYear[this.getMonth()];
+ };
+
+ Date.prototype.strftime = function(format) {
+ const fields = {
+ a: this.getAbbrevDayName(),
+ A: this.getFullDayName(),
+ b: this.getAbbrevMonthName(),
+ B: this.getFullMonthName(),
+ c: this.toString(),
+ d: this.getTwoDigitDate(),
+ H: this.getTwoDigitHour(),
+ I: this.getTwoDigitTwelveHour(),
+ m: this.getTwoDigitMonth(),
+ M: this.getTwoDigitMinute(),
+ p: (this.getHours() >= 12) ? 'PM' : 'AM',
+ S: this.getTwoDigitSecond(),
+ w: '0' + this.getDay(),
+ x: this.toLocaleDateString(),
+ X: this.toLocaleTimeString(),
+ y: ('' + this.getFullYear()).substr(2, 4),
+ Y: '' + this.getFullYear(),
+ '%': '%'
+ };
+ let result = '', i = 0;
+ while (i < format.length) {
+ if (format.charAt(i) === '%') {
+ result += fields[format.charAt(i + 1)];
+ ++i;
+ }
+ else {
+ result += format.charAt(i);
+ }
+ ++i;
+ }
+ return result;
+ };
+
+ // ----------------------------------------------------------------------------
+ // String object extensions
+ // ----------------------------------------------------------------------------
+ String.prototype.strptime = function(format) {
+ const split_format = format.split(/[.\-/]/);
+ const date = this.split(/[.\-/]/);
+ let i = 0;
+ let day, month, year;
+ while (i < split_format.length) {
+ switch (split_format[i]) {
+ case "%d":
+ day = date[i];
+ break;
+ case "%m":
+ month = date[i] - 1;
+ break;
+ case "%Y":
+ year = date[i];
+ break;
+ case "%y":
+ // A %y value in the range of [00, 68] is in the current
+ // century, while [69, 99] is in the previous century,
+ // according to the Open Group Specification.
+ if (parseInt(date[i], 10) >= 69) {
+ year = date[i];
+ } else {
+ year = (new Date(Date.UTC(date[i], 0))).getUTCFullYear() + 100;
+ }
+ break;
+ }
+ ++i;
+ }
+ // Create Date object from UTC since the parsed value is supposed to be
+ // in UTC, not local time. Also, the calendar uses UTC functions for
+ // date extraction.
+ return new Date(Date.UTC(year, month, day));
+ };
+}
diff --git a/project_manage/staticfiles/admin/js/filters.js b/project_manage/staticfiles/admin/js/filters.js
new file mode 100644
index 0000000..f5536eb
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/filters.js
@@ -0,0 +1,30 @@
+/**
+ * Persist changelist filters state (collapsed/expanded).
+ */
+'use strict';
+{
+ // Init filters.
+ let filters = JSON.parse(sessionStorage.getItem('django.admin.filtersState'));
+
+ if (!filters) {
+ filters = {};
+ }
+
+ Object.entries(filters).forEach(([key, value]) => {
+ const detailElement = document.querySelector(`[data-filter-title='${CSS.escape(key)}']`);
+
+ // Check if the filter is present, it could be from other view.
+ if (detailElement) {
+ value ? detailElement.setAttribute('open', '') : detailElement.removeAttribute('open');
+ }
+ });
+
+ // Save filter state when clicks.
+ const details = document.querySelectorAll('details');
+ details.forEach(detail => {
+ detail.addEventListener('toggle', event => {
+ filters[`${event.target.dataset.filterTitle}`] = detail.open;
+ sessionStorage.setItem('django.admin.filtersState', JSON.stringify(filters));
+ });
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/inlines.js b/project_manage/staticfiles/admin/js/inlines.js
new file mode 100644
index 0000000..e9a1dfe
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/inlines.js
@@ -0,0 +1,359 @@
+/*global DateTimeShortcuts, SelectFilter*/
+/**
+ * Django admin inlines
+ *
+ * Based on jQuery Formset 1.1
+ * @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.
+ *
+ * Spiced up with Code from Zain Memon's GSoC project 2009
+ * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip.
+ *
+ * Licensed under the New BSD License
+ * See: https://opensource.org/licenses/bsd-license.php
+ */
+'use strict';
+{
+ const $ = django.jQuery;
+ $.fn.formset = function(opts) {
+ const options = $.extend({}, $.fn.formset.defaults, opts);
+ const $this = $(this);
+ const $parent = $this.parent();
+ const updateElementIndex = function(el, prefix, ndx) {
+ const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
+ const replacement = prefix + "-" + ndx;
+ if ($(el).prop("for")) {
+ $(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
+ }
+ if (el.id) {
+ el.id = el.id.replace(id_regex, replacement);
+ }
+ if (el.name) {
+ el.name = el.name.replace(id_regex, replacement);
+ }
+ };
+ const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off");
+ let nextIndex = parseInt(totalForms.val(), 10);
+ const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off");
+ const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop("autocomplete", "off");
+ let addButton;
+
+ /**
+ * The "Add another MyModel" button below the inline forms.
+ */
+ const addInlineAddButton = function() {
+ if (addButton === null) {
+ if ($this.prop("tagName") === "TR") {
+ // If forms are laid out as table rows, insert the
+ // "add" button in a new table row:
+ const numCols = $this.eq(-1).children().length;
+ $parent.append('' + options.addText + " |
");
+ addButton = $parent.find("tr:last a");
+ } else {
+ // Otherwise, insert it immediately after the last form:
+ $this.filter(":last").after('");
+ addButton = $this.filter(":last").next().find("a");
+ }
+ }
+ addButton.on('click', addInlineClickHandler);
+ };
+
+ const addInlineClickHandler = function(e) {
+ e.preventDefault();
+ const template = $("#" + options.prefix + "-empty");
+ const row = template.clone(true);
+ row.removeClass(options.emptyCssClass)
+ .addClass(options.formCssClass)
+ .attr("id", options.prefix + "-" + nextIndex);
+ addInlineDeleteButton(row);
+ row.find("*").each(function() {
+ updateElementIndex(this, options.prefix, totalForms.val());
+ });
+ // Insert the new form when it has been fully edited.
+ row.insertBefore($(template));
+ // Update number of total forms.
+ $(totalForms).val(parseInt(totalForms.val(), 10) + 1);
+ nextIndex += 1;
+ // Hide the add button if there's a limit and it's been reached.
+ if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
+ addButton.parent().hide();
+ }
+ // Show the remove buttons if there are more than min_num.
+ toggleDeleteButtonVisibility(row.closest('.inline-group'));
+
+ // Pass the new form to the post-add callback, if provided.
+ if (options.added) {
+ options.added(row);
+ }
+ row.get(0).dispatchEvent(new CustomEvent("formset:added", {
+ bubbles: true,
+ detail: {
+ formsetName: options.prefix
+ }
+ }));
+ };
+
+ /**
+ * The "X" button that is part of every unsaved inline.
+ * (When saved, it is replaced with a "Delete" checkbox.)
+ */
+ const addInlineDeleteButton = function(row) {
+ if (row.is("tr")) {
+ // If the forms are laid out in table rows, insert
+ // the remove button into the last table cell:
+ row.children(":last").append('");
+ } else if (row.is("ul") || row.is("ol")) {
+ // If they're laid out as an ordered/unordered list,
+ // insert an after the last list item:
+ row.append('' + options.deleteText + "");
+ } else {
+ // Otherwise, just insert the remove button as the
+ // last child element of the form's container:
+ row.children(":first").append('' + options.deleteText + "");
+ }
+ // Add delete handler for each row.
+ row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this));
+ };
+
+ const inlineDeleteHandler = function(e1) {
+ e1.preventDefault();
+ const deleteButton = $(e1.target);
+ const row = deleteButton.closest('.' + options.formCssClass);
+ const inlineGroup = row.closest('.inline-group');
+ // Remove the parent form containing this button,
+ // and also remove the relevant row with non-field errors:
+ const prevRow = row.prev();
+ if (prevRow.length && prevRow.hasClass('row-form-errors')) {
+ prevRow.remove();
+ }
+ row.remove();
+ nextIndex -= 1;
+ // Pass the deleted form to the post-delete callback, if provided.
+ if (options.removed) {
+ options.removed(row);
+ }
+ document.dispatchEvent(new CustomEvent("formset:removed", {
+ detail: {
+ formsetName: options.prefix
+ }
+ }));
+ // Update the TOTAL_FORMS form count.
+ const forms = $("." + options.formCssClass);
+ $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
+ // Show add button again once below maximum number.
+ if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) {
+ addButton.parent().show();
+ }
+ // Hide the remove buttons if at min_num.
+ toggleDeleteButtonVisibility(inlineGroup);
+ // Also, update names and ids for all remaining form controls so
+ // they remain in sequence:
+ let i, formCount;
+ const updateElementCallback = function() {
+ updateElementIndex(this, options.prefix, i);
+ };
+ for (i = 0, formCount = forms.length; i < formCount; i++) {
+ updateElementIndex($(forms).get(i), options.prefix, i);
+ $(forms.get(i)).find("*").each(updateElementCallback);
+ }
+ };
+
+ const toggleDeleteButtonVisibility = function(inlineGroup) {
+ if ((minForms.val() !== '') && (minForms.val() - totalForms.val()) >= 0) {
+ inlineGroup.find('.inline-deletelink').hide();
+ } else {
+ inlineGroup.find('.inline-deletelink').show();
+ }
+ };
+
+ $this.each(function(i) {
+ $(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
+ });
+
+ // Create the delete buttons for all unsaved inlines:
+ $this.filter('.' + options.formCssClass + ':not(.has_original):not(.' + options.emptyCssClass + ')').each(function() {
+ addInlineDeleteButton($(this));
+ });
+ toggleDeleteButtonVisibility($this);
+
+ // Create the add button, initially hidden.
+ addButton = options.addButton;
+ addInlineAddButton();
+
+ // Show the add button if allowed to add more items.
+ // Note that max_num = None translates to a blank string.
+ const showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0;
+ if ($this.length && showAddButton) {
+ addButton.parent().show();
+ } else {
+ addButton.parent().hide();
+ }
+
+ return this;
+ };
+
+ /* Setup plugin defaults */
+ $.fn.formset.defaults = {
+ prefix: "form", // The form prefix for your django formset
+ addText: "add another", // Text for the add link
+ deleteText: "remove", // Text for the delete link
+ addCssClass: "add-row", // CSS class applied to the add link
+ deleteCssClass: "delete-row", // CSS class applied to the delete link
+ emptyCssClass: "empty-row", // CSS class applied to the empty row
+ formCssClass: "dynamic-form", // CSS class applied to each form in a formset
+ added: null, // Function called each time a new form is added
+ removed: null, // Function called each time a form is deleted
+ addButton: null // Existing add button to use
+ };
+
+
+ // Tabular inlines ---------------------------------------------------------
+ $.fn.tabularFormset = function(selector, options) {
+ const $rows = $(this);
+
+ const reinitDateTimeShortCuts = function() {
+ // Reinitialize the calendar and clock widgets by force
+ if (typeof DateTimeShortcuts !== "undefined") {
+ $(".datetimeshortcuts").remove();
+ DateTimeShortcuts.init();
+ }
+ };
+
+ const updateSelectFilter = function() {
+ // If any SelectFilter widgets are a part of the new form,
+ // instantiate a new SelectFilter instance for it.
+ if (typeof SelectFilter !== 'undefined') {
+ $('.selectfilter').each(function(index, value) {
+ SelectFilter.init(value.id, this.dataset.fieldName, false);
+ });
+ $('.selectfilterstacked').each(function(index, value) {
+ SelectFilter.init(value.id, this.dataset.fieldName, true);
+ });
+ }
+ };
+
+ const initPrepopulatedFields = function(row) {
+ row.find('.prepopulated_field').each(function() {
+ const field = $(this),
+ input = field.find('input, select, textarea'),
+ dependency_list = input.data('dependency_list') || [],
+ dependencies = [];
+ $.each(dependency_list, function(i, field_name) {
+ dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id'));
+ });
+ if (dependencies.length) {
+ input.prepopulate(dependencies, input.attr('maxlength'));
+ }
+ });
+ };
+
+ $rows.formset({
+ prefix: options.prefix,
+ addText: options.addText,
+ formCssClass: "dynamic-" + options.prefix,
+ deleteCssClass: "inline-deletelink",
+ deleteText: options.deleteText,
+ emptyCssClass: "empty-form",
+ added: function(row) {
+ initPrepopulatedFields(row);
+ reinitDateTimeShortCuts();
+ updateSelectFilter();
+ },
+ addButton: options.addButton
+ });
+
+ return $rows;
+ };
+
+ // Stacked inlines ---------------------------------------------------------
+ $.fn.stackedFormset = function(selector, options) {
+ const $rows = $(this);
+ const updateInlineLabel = function(row) {
+ $(selector).find(".inline_label").each(function(i) {
+ const count = i + 1;
+ $(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
+ });
+ };
+
+ const reinitDateTimeShortCuts = function() {
+ // Reinitialize the calendar and clock widgets by force, yuck.
+ if (typeof DateTimeShortcuts !== "undefined") {
+ $(".datetimeshortcuts").remove();
+ DateTimeShortcuts.init();
+ }
+ };
+
+ const updateSelectFilter = function() {
+ // If any SelectFilter widgets were added, instantiate a new instance.
+ if (typeof SelectFilter !== "undefined") {
+ $(".selectfilter").each(function(index, value) {
+ SelectFilter.init(value.id, this.dataset.fieldName, false);
+ });
+ $(".selectfilterstacked").each(function(index, value) {
+ SelectFilter.init(value.id, this.dataset.fieldName, true);
+ });
+ }
+ };
+
+ const initPrepopulatedFields = function(row) {
+ row.find('.prepopulated_field').each(function() {
+ const field = $(this),
+ input = field.find('input, select, textarea'),
+ dependency_list = input.data('dependency_list') || [],
+ dependencies = [];
+ $.each(dependency_list, function(i, field_name) {
+ // Dependency in a fieldset.
+ let field_element = row.find('.form-row .field-' + field_name);
+ // Dependency without a fieldset.
+ if (!field_element.length) {
+ field_element = row.find('.form-row.field-' + field_name);
+ }
+ dependencies.push('#' + field_element.find('input, select, textarea').attr('id'));
+ });
+ if (dependencies.length) {
+ input.prepopulate(dependencies, input.attr('maxlength'));
+ }
+ });
+ };
+
+ $rows.formset({
+ prefix: options.prefix,
+ addText: options.addText,
+ formCssClass: "dynamic-" + options.prefix,
+ deleteCssClass: "inline-deletelink",
+ deleteText: options.deleteText,
+ emptyCssClass: "empty-form",
+ removed: updateInlineLabel,
+ added: function(row) {
+ initPrepopulatedFields(row);
+ reinitDateTimeShortCuts();
+ updateSelectFilter();
+ updateInlineLabel(row);
+ },
+ addButton: options.addButton
+ });
+
+ return $rows;
+ };
+
+ $(document).ready(function() {
+ $(".js-inline-admin-formset").each(function() {
+ const data = $(this).data(),
+ inlineOptions = data.inlineFormset;
+ let selector;
+ switch(data.inlineType) {
+ case "stacked":
+ selector = inlineOptions.name + "-group .inline-related";
+ $(selector).stackedFormset(selector, inlineOptions.options);
+ break;
+ case "tabular":
+ selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row";
+ $(selector).tabularFormset(selector, inlineOptions.options);
+ break;
+ }
+ });
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/jquery.init.js b/project_manage/staticfiles/admin/js/jquery.init.js
new file mode 100644
index 0000000..f40b27f
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/jquery.init.js
@@ -0,0 +1,8 @@
+/*global jQuery:false*/
+'use strict';
+/* Puts the included jQuery into our own namespace using noConflict and passing
+ * it 'true'. This ensures that the included jQuery doesn't pollute the global
+ * namespace (i.e. this preserves pre-existing values for both window.$ and
+ * window.jQuery).
+ */
+window.django = {jQuery: jQuery.noConflict(true)};
diff --git a/project_manage/staticfiles/admin/js/nav_sidebar.js b/project_manage/staticfiles/admin/js/nav_sidebar.js
new file mode 100644
index 0000000..7e735db
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/nav_sidebar.js
@@ -0,0 +1,79 @@
+'use strict';
+{
+ const toggleNavSidebar = document.getElementById('toggle-nav-sidebar');
+ if (toggleNavSidebar !== null) {
+ const navSidebar = document.getElementById('nav-sidebar');
+ const main = document.getElementById('main');
+ let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen');
+ if (navSidebarIsOpen === null) {
+ navSidebarIsOpen = 'true';
+ }
+ main.classList.toggle('shifted', navSidebarIsOpen === 'true');
+ navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);
+
+ toggleNavSidebar.addEventListener('click', function() {
+ if (navSidebarIsOpen === 'true') {
+ navSidebarIsOpen = 'false';
+ } else {
+ navSidebarIsOpen = 'true';
+ }
+ localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen);
+ main.classList.toggle('shifted');
+ navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);
+ });
+ }
+
+ function initSidebarQuickFilter() {
+ const options = [];
+ const navSidebar = document.getElementById('nav-sidebar');
+ if (!navSidebar) {
+ return;
+ }
+ navSidebar.querySelectorAll('th[scope=row] a').forEach((container) => {
+ options.push({title: container.innerHTML, node: container});
+ });
+
+ function checkValue(event) {
+ let filterValue = event.target.value;
+ if (filterValue) {
+ filterValue = filterValue.toLowerCase();
+ }
+ if (event.key === 'Escape') {
+ filterValue = '';
+ event.target.value = ''; // clear input
+ }
+ let matches = false;
+ for (const o of options) {
+ let displayValue = '';
+ if (filterValue) {
+ if (o.title.toLowerCase().indexOf(filterValue) === -1) {
+ displayValue = 'none';
+ } else {
+ matches = true;
+ }
+ }
+ // show/hide parent
+ o.node.parentNode.parentNode.style.display = displayValue;
+ }
+ if (!filterValue || matches) {
+ event.target.classList.remove('no-results');
+ } else {
+ event.target.classList.add('no-results');
+ }
+ sessionStorage.setItem('django.admin.navSidebarFilterValue', filterValue);
+ }
+
+ const nav = document.getElementById('nav-filter');
+ nav.addEventListener('change', checkValue, false);
+ nav.addEventListener('input', checkValue, false);
+ nav.addEventListener('keyup', checkValue, false);
+
+ const storedValue = sessionStorage.getItem('django.admin.navSidebarFilterValue');
+ if (storedValue) {
+ nav.value = storedValue;
+ checkValue({target: nav, key: ''});
+ }
+ }
+ window.initSidebarQuickFilter = initSidebarQuickFilter;
+ initSidebarQuickFilter();
+}
diff --git a/project_manage/staticfiles/admin/js/popup_response.js b/project_manage/staticfiles/admin/js/popup_response.js
new file mode 100644
index 0000000..2b1d3dd
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/popup_response.js
@@ -0,0 +1,16 @@
+/*global opener */
+'use strict';
+{
+ const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse);
+ switch(initData.action) {
+ case 'change':
+ opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value);
+ break;
+ case 'delete':
+ opener.dismissDeleteRelatedObjectPopup(window, initData.value);
+ break;
+ default:
+ opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
+ break;
+ }
+}
diff --git a/project_manage/staticfiles/admin/js/prepopulate.js b/project_manage/staticfiles/admin/js/prepopulate.js
new file mode 100644
index 0000000..89e95ab
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/prepopulate.js
@@ -0,0 +1,43 @@
+/*global URLify*/
+'use strict';
+{
+ const $ = django.jQuery;
+ $.fn.prepopulate = function(dependencies, maxLength, allowUnicode) {
+ /*
+ Depends on urlify.js
+ Populates a selected field with the values of the dependent fields,
+ URLifies and shortens the string.
+ dependencies - array of dependent fields ids
+ maxLength - maximum length of the URLify'd string
+ allowUnicode - Unicode support of the URLify'd string
+ */
+ return this.each(function() {
+ const prepopulatedField = $(this);
+
+ const populate = function() {
+ // Bail if the field's value has been changed by the user
+ if (prepopulatedField.data('_changed')) {
+ return;
+ }
+
+ const values = [];
+ $.each(dependencies, function(i, field) {
+ field = $(field);
+ if (field.val().length > 0) {
+ values.push(field.val());
+ }
+ });
+ prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode));
+ };
+
+ prepopulatedField.data('_changed', false);
+ prepopulatedField.on('change', function() {
+ prepopulatedField.data('_changed', true);
+ });
+
+ if (!prepopulatedField.val()) {
+ $(dependencies.join(',')).on('keyup change focus', populate);
+ }
+ });
+ };
+}
diff --git a/project_manage/staticfiles/admin/js/prepopulate_init.js b/project_manage/staticfiles/admin/js/prepopulate_init.js
new file mode 100644
index 0000000..a58841f
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/prepopulate_init.js
@@ -0,0 +1,15 @@
+'use strict';
+{
+ const $ = django.jQuery;
+ const fields = $('#django-admin-prepopulated-fields-constants').data('prepopulatedFields');
+ $.each(fields, function(index, field) {
+ $(
+ '.empty-form .form-row .field-' + field.name +
+ ', .empty-form.form-row .field-' + field.name +
+ ', .empty-form .form-row.field-' + field.name
+ ).addClass('prepopulated_field');
+ $(field.id).data('dependency_list', field.dependency_list).prepopulate(
+ field.dependency_ids, field.maxLength, field.allowUnicode
+ );
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/theme.js b/project_manage/staticfiles/admin/js/theme.js
new file mode 100644
index 0000000..794cd15
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/theme.js
@@ -0,0 +1,56 @@
+'use strict';
+{
+ window.addEventListener('load', function(e) {
+
+ function setTheme(mode) {
+ if (mode !== "light" && mode !== "dark" && mode !== "auto") {
+ console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`);
+ mode = "auto";
+ }
+ document.documentElement.dataset.theme = mode;
+ localStorage.setItem("theme", mode);
+ }
+
+ function cycleTheme() {
+ const currentTheme = localStorage.getItem("theme") || "auto";
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+
+ if (prefersDark) {
+ // Auto (dark) -> Light -> Dark
+ if (currentTheme === "auto") {
+ setTheme("light");
+ } else if (currentTheme === "light") {
+ setTheme("dark");
+ } else {
+ setTheme("auto");
+ }
+ } else {
+ // Auto (light) -> Dark -> Light
+ if (currentTheme === "auto") {
+ setTheme("dark");
+ } else if (currentTheme === "dark") {
+ setTheme("light");
+ } else {
+ setTheme("auto");
+ }
+ }
+ }
+
+ function initTheme() {
+ // set theme defined in localStorage if there is one, or fallback to auto mode
+ const currentTheme = localStorage.getItem("theme");
+ currentTheme ? setTheme(currentTheme) : setTheme("auto");
+ }
+
+ function setupTheme() {
+ // Attach event handlers for toggling themes
+ const buttons = document.getElementsByClassName("theme-toggle");
+ Array.from(buttons).forEach((btn) => {
+ btn.addEventListener("click", cycleTheme);
+ });
+ initTheme();
+ }
+
+ setupTheme();
+ });
+}
diff --git a/project_manage/staticfiles/admin/js/urlify.js b/project_manage/staticfiles/admin/js/urlify.js
new file mode 100644
index 0000000..9fc0409
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/urlify.js
@@ -0,0 +1,169 @@
+/*global XRegExp*/
+'use strict';
+{
+ const LATIN_MAP = {
+ 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE',
+ 'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I',
+ 'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O',
+ 'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U',
+ 'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a',
+ 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c',
+ 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i',
+ 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o',
+ 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u',
+ 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
+ };
+ const LATIN_SYMBOLS_MAP = {
+ '©': '(c)'
+ };
+ const GREEK_MAP = {
+ 'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h',
+ 'θ': '8', 'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': '3',
+ 'ο': 'o', 'π': 'p', 'ρ': 'r', 'σ': 's', 'τ': 't', 'υ': 'y', 'φ': 'f',
+ 'χ': 'x', 'ψ': 'ps', 'ω': 'w', 'ά': 'a', 'έ': 'e', 'ί': 'i', 'ό': 'o',
+ 'ύ': 'y', 'ή': 'h', 'ώ': 'w', 'ς': 's', 'ϊ': 'i', 'ΰ': 'y', 'ϋ': 'y',
+ 'ΐ': 'i', 'Α': 'A', 'Β': 'B', 'Γ': 'G', 'Δ': 'D', 'Ε': 'E', 'Ζ': 'Z',
+ 'Η': 'H', 'Θ': '8', 'Ι': 'I', 'Κ': 'K', 'Λ': 'L', 'Μ': 'M', 'Ν': 'N',
+ 'Ξ': '3', 'Ο': 'O', 'Π': 'P', 'Ρ': 'R', 'Σ': 'S', 'Τ': 'T', 'Υ': 'Y',
+ 'Φ': 'F', 'Χ': 'X', 'Ψ': 'PS', 'Ω': 'W', 'Ά': 'A', 'Έ': 'E', 'Ί': 'I',
+ 'Ό': 'O', 'Ύ': 'Y', 'Ή': 'H', 'Ώ': 'W', 'Ϊ': 'I', 'Ϋ': 'Y'
+ };
+ const TURKISH_MAP = {
+ 'ş': 's', 'Ş': 'S', 'ı': 'i', 'İ': 'I', 'ç': 'c', 'Ç': 'C', 'ü': 'u',
+ 'Ü': 'U', 'ö': 'o', 'Ö': 'O', 'ğ': 'g', 'Ğ': 'G'
+ };
+ const ROMANIAN_MAP = {
+ 'ă': 'a', 'î': 'i', 'ș': 's', 'ț': 't', 'â': 'a',
+ 'Ă': 'A', 'Î': 'I', 'Ș': 'S', 'Ț': 'T', 'Â': 'A'
+ };
+ const RUSSIAN_MAP = {
+ 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
+ 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm',
+ 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
+ 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sh', 'ъ': '',
+ 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
+ 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo',
+ 'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M',
+ 'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U',
+ 'Ф': 'F', 'Х': 'H', 'Ц': 'C', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sh', 'Ъ': '',
+ 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya'
+ };
+ const UKRAINIAN_MAP = {
+ 'Є': 'Ye', 'І': 'I', 'Ї': 'Yi', 'Ґ': 'G', 'є': 'ye', 'і': 'i',
+ 'ї': 'yi', 'ґ': 'g'
+ };
+ const CZECH_MAP = {
+ 'č': 'c', 'ď': 'd', 'ě': 'e', 'ň': 'n', 'ř': 'r', 'š': 's', 'ť': 't',
+ 'ů': 'u', 'ž': 'z', 'Č': 'C', 'Ď': 'D', 'Ě': 'E', 'Ň': 'N', 'Ř': 'R',
+ 'Š': 'S', 'Ť': 'T', 'Ů': 'U', 'Ž': 'Z'
+ };
+ const SLOVAK_MAP = {
+ 'á': 'a', 'ä': 'a', 'č': 'c', 'ď': 'd', 'é': 'e', 'í': 'i', 'ľ': 'l',
+ 'ĺ': 'l', 'ň': 'n', 'ó': 'o', 'ô': 'o', 'ŕ': 'r', 'š': 's', 'ť': 't',
+ 'ú': 'u', 'ý': 'y', 'ž': 'z',
+ 'Á': 'a', 'Ä': 'A', 'Č': 'C', 'Ď': 'D', 'É': 'E', 'Í': 'I', 'Ľ': 'L',
+ 'Ĺ': 'L', 'Ň': 'N', 'Ó': 'O', 'Ô': 'O', 'Ŕ': 'R', 'Š': 'S', 'Ť': 'T',
+ 'Ú': 'U', 'Ý': 'Y', 'Ž': 'Z'
+ };
+ const POLISH_MAP = {
+ 'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's',
+ 'ź': 'z', 'ż': 'z',
+ 'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N', 'Ó': 'O', 'Ś': 'S',
+ 'Ź': 'Z', 'Ż': 'Z'
+ };
+ const LATVIAN_MAP = {
+ 'ā': 'a', 'č': 'c', 'ē': 'e', 'ģ': 'g', 'ī': 'i', 'ķ': 'k', 'ļ': 'l',
+ 'ņ': 'n', 'š': 's', 'ū': 'u', 'ž': 'z',
+ 'Ā': 'A', 'Č': 'C', 'Ē': 'E', 'Ģ': 'G', 'Ī': 'I', 'Ķ': 'K', 'Ļ': 'L',
+ 'Ņ': 'N', 'Š': 'S', 'Ū': 'U', 'Ž': 'Z'
+ };
+ const ARABIC_MAP = {
+ 'أ': 'a', 'ب': 'b', 'ت': 't', 'ث': 'th', 'ج': 'g', 'ح': 'h', 'خ': 'kh', 'د': 'd',
+ 'ذ': 'th', 'ر': 'r', 'ز': 'z', 'س': 's', 'ش': 'sh', 'ص': 's', 'ض': 'd', 'ط': 't',
+ 'ظ': 'th', 'ع': 'aa', 'غ': 'gh', 'ف': 'f', 'ق': 'k', 'ك': 'k', 'ل': 'l', 'م': 'm',
+ 'ن': 'n', 'ه': 'h', 'و': 'o', 'ي': 'y'
+ };
+ const LITHUANIAN_MAP = {
+ 'ą': 'a', 'č': 'c', 'ę': 'e', 'ė': 'e', 'į': 'i', 'š': 's', 'ų': 'u',
+ 'ū': 'u', 'ž': 'z',
+ 'Ą': 'A', 'Č': 'C', 'Ę': 'E', 'Ė': 'E', 'Į': 'I', 'Š': 'S', 'Ų': 'U',
+ 'Ū': 'U', 'Ž': 'Z'
+ };
+ const SERBIAN_MAP = {
+ 'ђ': 'dj', 'ј': 'j', 'љ': 'lj', 'њ': 'nj', 'ћ': 'c', 'џ': 'dz',
+ 'đ': 'dj', 'Ђ': 'Dj', 'Ј': 'j', 'Љ': 'Lj', 'Њ': 'Nj', 'Ћ': 'C',
+ 'Џ': 'Dz', 'Đ': 'Dj'
+ };
+ const AZERBAIJANI_MAP = {
+ 'ç': 'c', 'ə': 'e', 'ğ': 'g', 'ı': 'i', 'ö': 'o', 'ş': 's', 'ü': 'u',
+ 'Ç': 'C', 'Ə': 'E', 'Ğ': 'G', 'İ': 'I', 'Ö': 'O', 'Ş': 'S', 'Ü': 'U'
+ };
+ const GEORGIAN_MAP = {
+ 'ა': 'a', 'ბ': 'b', 'გ': 'g', 'დ': 'd', 'ე': 'e', 'ვ': 'v', 'ზ': 'z',
+ 'თ': 't', 'ი': 'i', 'კ': 'k', 'ლ': 'l', 'მ': 'm', 'ნ': 'n', 'ო': 'o',
+ 'პ': 'p', 'ჟ': 'j', 'რ': 'r', 'ს': 's', 'ტ': 't', 'უ': 'u', 'ფ': 'f',
+ 'ქ': 'q', 'ღ': 'g', 'ყ': 'y', 'შ': 'sh', 'ჩ': 'ch', 'ც': 'c', 'ძ': 'dz',
+ 'წ': 'w', 'ჭ': 'ch', 'ხ': 'x', 'ჯ': 'j', 'ჰ': 'h'
+ };
+
+ const ALL_DOWNCODE_MAPS = [
+ LATIN_MAP,
+ LATIN_SYMBOLS_MAP,
+ GREEK_MAP,
+ TURKISH_MAP,
+ ROMANIAN_MAP,
+ RUSSIAN_MAP,
+ UKRAINIAN_MAP,
+ CZECH_MAP,
+ SLOVAK_MAP,
+ POLISH_MAP,
+ LATVIAN_MAP,
+ ARABIC_MAP,
+ LITHUANIAN_MAP,
+ SERBIAN_MAP,
+ AZERBAIJANI_MAP,
+ GEORGIAN_MAP
+ ];
+
+ const Downcoder = {
+ 'Initialize': function() {
+ if (Downcoder.map) { // already made
+ return;
+ }
+ Downcoder.map = {};
+ for (const lookup of ALL_DOWNCODE_MAPS) {
+ Object.assign(Downcoder.map, lookup);
+ }
+ Downcoder.regex = new RegExp(Object.keys(Downcoder.map).join('|'), 'g');
+ }
+ };
+
+ function downcode(slug) {
+ Downcoder.Initialize();
+ return slug.replace(Downcoder.regex, function(m) {
+ return Downcoder.map[m];
+ });
+ }
+
+
+ function URLify(s, num_chars, allowUnicode) {
+ // changes, e.g., "Petty theft" to "petty-theft"
+ if (!allowUnicode) {
+ s = downcode(s);
+ }
+ s = s.toLowerCase(); // convert to lowercase
+ // if downcode doesn't hit, the char will be stripped here
+ if (allowUnicode) {
+ // Keep Unicode letters including both lowercase and uppercase
+ // characters, whitespace, and dash; remove other characters.
+ s = XRegExp.replace(s, XRegExp('[^-_\\p{L}\\p{N}\\s]', 'g'), '');
+ } else {
+ s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars
+ }
+ s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces
+ s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens
+ s = s.substring(0, num_chars); // trim to first num_chars chars
+ return s.replace(/-+$/g, ''); // trim any trailing hyphens
+ }
+ window.URLify = URLify;
+}
diff --git a/project_manage/staticfiles/admin/js/vendor/jquery/LICENSE.txt b/project_manage/staticfiles/admin/js/vendor/jquery/LICENSE.txt
new file mode 100644
index 0000000..f642c3f
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/vendor/jquery/LICENSE.txt
@@ -0,0 +1,20 @@
+Copyright OpenJS Foundation and other contributors, https://openjsf.org/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/project_manage/staticfiles/admin/js/vendor/jquery/jquery.js b/project_manage/staticfiles/admin/js/vendor/jquery/jquery.js
new file mode 100644
index 0000000..1a86433
--- /dev/null
+++ b/project_manage/staticfiles/admin/js/vendor/jquery/jquery.js
@@ -0,0 +1,10716 @@
+/*!
+ * jQuery JavaScript Library v3.7.1
+ * https://jquery.com/
+ *
+ * Copyright OpenJS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2023-08-28T13:37Z
+ */
+( function( global, factory ) {
+
+ "use strict";
+
+ if ( typeof module === "object" && typeof module.exports === "object" ) {
+
+ // For CommonJS and CommonJS-like environments where a proper `window`
+ // is present, execute the factory and get jQuery.
+ // For environments that do not have a `window` with a `document`
+ // (such as Node.js), expose a factory as module.exports.
+ // This accentuates the need for the creation of a real `window`.
+ // e.g. var jQuery = require("jquery")(window);
+ // See ticket trac-14549 for more info.
+ module.exports = global.document ?
+ factory( global, true ) :
+ function( w ) {
+ if ( !w.document ) {
+ throw new Error( "jQuery requires a window with a document" );
+ }
+ return factory( w );
+ };
+ } else {
+ factory( global );
+ }
+
+// Pass this if window is not defined yet
+} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
+
+// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
+// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
+// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
+// enough that all such attempts are guarded in a try block.
+"use strict";
+
+var arr = [];
+
+var getProto = Object.getPrototypeOf;
+
+var slice = arr.slice;
+
+var flat = arr.flat ? function( array ) {
+ return arr.flat.call( array );
+} : function( array ) {
+ return arr.concat.apply( [], array );
+};
+
+
+var push = arr.push;
+
+var indexOf = arr.indexOf;
+
+var class2type = {};
+
+var toString = class2type.toString;
+
+var hasOwn = class2type.hasOwnProperty;
+
+var fnToString = hasOwn.toString;
+
+var ObjectFunctionString = fnToString.call( Object );
+
+var support = {};
+
+var isFunction = function isFunction( obj ) {
+
+ // Support: Chrome <=57, Firefox <=52
+ // In some browsers, typeof returns "function" for HTML