diff --git a/.gitignore b/.gitignore
index f222afb..49fe236 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@
# from test projects:
**/static/*
+!src/inventory/static/*
**/media/*
*.sqlite3
diff --git a/src/inventory/static/inventory.js b/src/inventory/static/inventory.js
new file mode 100644
index 0000000..36703a8
--- /dev/null
+++ b/src/inventory/static/inventory.js
@@ -0,0 +1,16 @@
+(function ($) {
+ 'use strict';
+ $(function () {
+ const form_object = $(form_selector);
+ form_object.dirty({
+ preventLeaving: true,
+ onDirty: function () {
+ console.log('form is dirty');
+ var dirty_fields = form_object.dirty("showDirtyFields");
+ dirty_fields.each(function (index, element) {
+ console.log(index + ' - ' + element.value);
+ });
+ },
+ });
+ });
+})(django.jQuery);
\ No newline at end of file
diff --git a/src/inventory/static/jquery.dirty.js b/src/inventory/static/jquery.dirty.js
new file mode 100644
index 0000000..8c78422
--- /dev/null
+++ b/src/inventory/static/jquery.dirty.js
@@ -0,0 +1,308 @@
+/*
+ * Dirty
+ * jquery plugin to detect when a form is modified
+ * (c) 2016 Simon Taite - https://github.com/simon-reynolds/jquery.dirty
+ * originally based on jquery.dirrty by Ruben Torres - https://github.com/rubentd/dirrty
+ * Released under the MIT license
+ */
+
+(function($) {
+
+ //Save dirty instances
+ var singleDs = [];
+ var dirty = "dirty";
+ var clean = "clean";
+ var dataInitialValue = "dirtyInitialValue";
+ var dataIsDirty = "isDirty";
+
+ var getSingleton = function(id) {
+ var result;
+ singleDs.forEach(function(e) {
+ if (e.id === id) {
+ result = e;
+ }
+ });
+ return result;
+ };
+
+ var setSubmitEvents = function(d) {
+ d.form.on("submit", function() {
+ d.submitting = true;
+ });
+
+ if (d.options.preventLeaving) {
+ $(window).on("beforeunload", function(event) {
+ if (d.isDirty && !d.submitting) {
+ event.preventDefault();
+ return d.options.leavingMessage;
+ }
+ });
+ }
+ };
+
+ var setNamespacedEvents = function(d) {
+
+ d.form.find("input, select, textarea").on("change.dirty click.dirty keyup.dirty keydown.dirty blur.dirty", function(e) {
+ d.checkValues(e);
+ });
+
+ d.form.on("dirty", function() {
+ d.options.onDirty();
+ });
+
+ d.form.on("clean", function() {
+ d.options.onClean();
+ });
+ };
+
+ var clearNamespacedEvents = function(d) {
+ d.form.find("input, select, textarea").off("change.dirty click.dirty keyup.dirty keydown.dirty blur.dirty");
+
+ d.form.off("dirty");
+
+ d.form.off("clean");
+ };
+
+ var Dirty = function(form, options) {
+ this.form = form;
+ this.isDirty = false;
+ this.options = options;
+ this.history = [clean, clean]; //Keep track of last statuses
+ this.id = $(form).attr("id");
+ singleDs.push(this);
+ };
+
+ Dirty.prototype = {
+ init: function() {
+ this.saveInitialValues();
+ this.setEvents();
+ },
+
+ isRadioOrCheckbox: function(el){
+ return $(el).is(":radio, :checkbox");
+ },
+
+ isFileInput: function(el){
+ return $(el).is(":file")
+ },
+
+ saveInitialValues: function() {
+ var d = this;
+ this.form.find("input, select, textarea").each(function(_, e) {
+
+ var isRadioOrCheckbox = d.isRadioOrCheckbox(e);
+ var isFile = d.isFileInput(e);
+
+ if (isRadioOrCheckbox) {
+ var isChecked = $(e).is(":checked") ? "checked" : "unchecked";
+ $(e).data(dataInitialValue, isChecked);
+ } else if(isFile){
+ $(e).data(dataInitialValue, JSON.stringify(e.files))
+ } else {
+ $(e).data(dataInitialValue, $(e).val() || '');
+ }
+ });
+ },
+
+ refreshEvents: function () {
+ var d = this;
+ clearNamespacedEvents(d);
+ setNamespacedEvents(d);
+ },
+
+ showDirtyFields: function() {
+ var d = this;
+
+ return d.form.find("input, select, textarea").filter(function(_, e){
+ return $(e).data("isDirty");
+ });
+ },
+
+ setEvents: function() {
+ var d = this;
+
+ setSubmitEvents(d);
+ setNamespacedEvents(d);
+ },
+
+ isFieldDirty: function($field) {
+ var initialValue = $field.data(dataInitialValue);
+ // Explicitly check for null/undefined here as value may be `false`, so ($field.data(dataInitialValue) || '') would not work
+ if (initialValue == null) { initialValue = ''; }
+ var currentValue = $field.val();
+ if (currentValue == null) { currentValue = ''; }
+
+ // Boolean values can be encoded as "true/false" or "True/False" depending on underlying frameworks so we need a case insensitive comparison
+ var boolRegex = /^(true|false)$/i;
+ var isBoolValue = boolRegex.test(initialValue) && boolRegex.test(currentValue);
+ if (isBoolValue) {
+ var regex = new RegExp("^" + initialValue + "$", "i");
+ return !regex.test(currentValue);
+ }
+
+ return currentValue !== initialValue;
+ },
+
+ isFileInputDirty: function($field) {
+ var initialValue = $field.data(dataInitialValue);
+
+ var plainField = $field[0];
+ var currentValue = JSON.stringify(plainField.files);
+
+ return currentValue !== initialValue;
+ },
+
+ isCheckboxDirty: function($field) {
+ var initialValue = $field.data(dataInitialValue);
+ var currentValue = $field.is(":checked") ? "checked" : "unchecked";
+
+ return initialValue !== currentValue;
+ },
+
+ checkValues: function(e) {
+ var d = this;
+ var formIsDirty = false;
+
+ this.form.find("input, select, textarea").each(function(_, el) {
+ var isRadioOrCheckbox = d.isRadioOrCheckbox(el);
+ var isFile = d.isFileInput(el);
+ var $el = $(el);
+
+ var thisIsDirty;
+ if (isRadioOrCheckbox) {
+ thisIsDirty = d.isCheckboxDirty($el);
+ } else if (isFile) {
+ thisIsDirty = d.isFileInputDirty($el);
+ } else {
+ thisIsDirty = d.isFieldDirty($el);
+ }
+
+ $el.data(dataIsDirty, thisIsDirty);
+
+ formIsDirty |= thisIsDirty;
+ });
+
+ if (formIsDirty) {
+ d.setDirty();
+ } else {
+ d.setClean();
+ }
+ },
+
+ setDirty: function() {
+ this.isDirty = true;
+ this.history[0] = this.history[1];
+ this.history[1] = dirty;
+
+ if (this.options.fireEventsOnEachChange || this.wasJustClean()) {
+ this.form.trigger("dirty");
+ }
+ },
+
+ setClean: function() {
+ this.isDirty = false;
+ this.history[0] = this.history[1];
+ this.history[1] = clean;
+
+ if (this.options.fireEventsOnEachChange || this.wasJustDirty()) {
+ this.form.trigger("clean");
+ }
+ },
+
+ //Lets me know if the previous status of the form was dirty
+ wasJustDirty: function() {
+ return (this.history[0] === dirty);
+ },
+
+ //Lets me know if the previous status of the form was clean
+ wasJustClean: function() {
+ return (this.history[0] === clean);
+ },
+
+ setAsClean: function(){
+ this.saveInitialValues();
+ this.setClean();
+ },
+
+ setAsDirty: function(){
+ this.saveInitialValues();
+ this.setDirty();
+ },
+
+ resetForm: function(){
+ var d = this;
+ this.form.find("input, select, textarea").each(function(_, e) {
+
+ var $e = $(e);
+ var isRadioOrCheckbox = d.isRadioOrCheckbox(e);
+ var isFile = d.isFileInput(e);
+
+ if (isRadioOrCheckbox) {
+ var initialCheckedState = $e.data(dataInitialValue);
+ var isChecked = initialCheckedState === "checked";
+
+ $e.prop("checked", isChecked);
+ } if(isFile) {
+ e.value = "";
+ $(e).data(dataInitialValue, JSON.stringify(e.files))
+
+ } else {
+ var value = $e.data(dataInitialValue);
+ $e.val(value);
+ }
+ });
+
+ this.checkValues();
+ }
+ };
+
+ $.fn.dirty = function(options) {
+
+ if (typeof options === "string" && /^(isDirty|isClean|refreshEvents|resetForm|setAsClean|setAsDirty|showDirtyFields)$/i.test(options)) {
+ //Check if we have an instance of dirty for this form
+ // TODO: check if this is DOM or jQuery object
+ var d = getSingleton($(this).attr("id"));
+
+ if (!d) {
+ d = new Dirty($(this), options);
+ d.init();
+ }
+ var optionsLowerCase = options.toLowerCase();
+
+ switch (optionsLowerCase) {
+ case "isclean":
+ return !d.isDirty;
+ case "isdirty":
+ return d.isDirty;
+ case "refreshevents":
+ d.refreshEvents();
+ case "resetform":
+ d.resetForm();
+ case "setasclean":
+ return d.setAsClean();
+ case "setasdirty":
+ return d.setAsDirty();
+ case "showdirtyfields":
+ return d.showDirtyFields();
+ }
+
+ } else if (typeof options === "object" || !options) {
+
+ return this.each(function(_, e) {
+ options = $.extend({}, $.fn.dirty.defaults, options);
+ var dirty = new Dirty($(e), options);
+ dirty.init();
+ });
+
+ }
+ };
+
+ $.fn.dirty.defaults = {
+ preventLeaving: false,
+ leavingMessage: "There are unsaved changes on this page which will be discarded if you continue.",
+ onDirty: $.noop, //This function is fired when the form gets dirty
+ onClean: $.noop, //This funciton is fired when the form gets clean again
+ fireEventsOnEachChange: false, // Fire onDirty/onClean on each modification of the form
+ };
+
+})(jQuery);
diff --git a/src/inventory_project/templates/admin/change_form.html b/src/inventory_project/templates/admin/change_form.html
new file mode 100644
index 0000000..f529ed6
--- /dev/null
+++ b/src/inventory_project/templates/admin/change_form.html
@@ -0,0 +1,10 @@
+{% extends "admin/change_form.html" %}
+{% load static %}
+
+{% block extrahead %}{{ block.super }}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html
index 9ea1c8a..92f6174 100644
--- a/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html
+++ b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html
@@ -71,6 +71,13 @@
+
+
+
diff --git a/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html
index c29a0ab..5a8724e 100644
--- a/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html
+++ b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html
@@ -71,6 +71,13 @@
+
+
+
diff --git a/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html b/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html
index d437a1c..f042341 100644
--- a/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html
+++ b/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html
@@ -67,6 +67,13 @@
+
+
+