Mike Slinn

Ancient Warmth pricing Django-Oscar App

Published 2021-03-03.
Time to read: 2 minutes.

This page is part of the ancientWarmth collection.

Private, for my own use only. Do not publish!

Nomenclature

Things

Mixture
Specifies only chemical parts/percentages
Design
Specifies the combination of:
  1. mixture
  2. essential oils (mL/L)
  3. color
  4. other non-chemical additives
  5. skin
Package
Design quantity (total grams), placed into a container, with a label affixed, after manufacturing.

People

User
someone with an Ancient Warmth login id.
Bather
User who obtained a package for their personal use. Users become bathers after their first purchase.
Designer
User who customized a design. It is possible to be a designer without being a bather - just don’t order a package.

Mixture

  • Has optional description
  • Has a designer’s handle, a name and a version
    userHandle:Hot spring:xxx
  • Heritage is tracked (copied from userX:blah blah:xxx, originated from userY:blah blah)
  • Classified into a taxonomy? (Ancient Warmth defines taxonomy)

Design

  • Mixture customized by adding items that do not affect the chemistry, but do affect the bather’s experience
  • Also has an optional description
  • Classified into one or more taxonomies (Ancient Warmth defines taxonomy structure)
    • Therapy / skin / eczema
    • Relaxation / silky
    • Relaxation / hot spring
    • Relaxation / sleep well
  • Heritage is tracked (copied from xxx, originated as yyy)
  • Default skin can be redefined by designer
  • Search by word
  • Sort by popularity
    • Popular near me
    • Show geographic recipe popularity
  • Share recipe link
  • RSS design feed

Users

  • Every user has an avatar or photo
  • Can order packaged designs
  • Can share designs with others

Bathers

  • Can rank designs they ordered
  • Get one point for ranking a design

Designers

  • Display top x% for their recipes
  • Grams sold per recipe or for all clones
  • Units sold per recipe or for all clones
  • Number of clones
  • Top x% in city / region / country / world
  • Can upload skins, a default skin and a skin for each of their mixtures or designs
  • The designer called Original is an alias for Ancient Warmth
  • Get one point for each star in a ranking
  • Should designers get points for # grams shipped of their designs?
  • Have a mission statement

Leaderboards

  • Top designers
  • Top designs
  • Trending designs
  • Trending designers

JSON data

Used for proof of concept video demo. Property names should be camelCase, this standard had not been established for the video demo. Any property names that are still snake cased should be renamed ASAP; currently only mixtures.json exhibits this issue.

Chemicals

Chemical formula contains UTF-8 characters, so superscripts and subscripts display well. In the video demo, fullDescription were arrays of short text lines. For the real product, fullDescription will be HTML fragments.

chemicals.json
[
  {
    "altNames": [ "Salt for diabetics", "reduced-sodium replacement salt", "NoSalt", "Nu-Salt", "No Salt Original" ],
    "formula": "KCl",
    "fullDescription": [
      "European 2009/54/EC Directive classifies mineral waters with more than 200 mg/L of chloride, as ‘water with chloride’."
    ],
    "fullName": "Potassium Chloride",
    "id": "KCl",
    "shortDescription": "This is a short description."
  },

  {
    "altNames": [ "Magnesium flake" ],
    "formula": "MgCl",
    "fullDescription": [
      "Dissolved magnesium makes water harder.",
      "European 2009/54/EC Directive classifies mineral waters with more than 50 mg/L of magnesium as ‘water with magnesium’,",
      "and they classify mineral waters with more than 50 mg/L of chloride as 'water with chloride'."
    ],
    "fullName": "Magnesium Chloride",
    "id": "MgCl",
    "shortDescription": "This is a short description."
  },

  {
    "altNames": [ "Epsom salt", "magnesium sulphate", "magnesium sulfate heptahydrate" ],
    "formula": "MgSO₄·7H₂O",
    "fullDescription": [
      "<p>",
        "Magnesium sulfate is commonly used in bath salts, exfoliants, muscle relaxers and pain relievers.",
        "It is often used in foot baths to soothe sore feet, and to hasten recovery from muscle pain, soreness, or injury.",
      "</p>",
      "<p>",
        "Does not dissolve as easily as magnesium chloride, and is not absorbed through the skin as easily either.",
        "This chemical is actually half water by weight, so magnesium chloride costs less to ship.",
      "</p>",
      "<p>",
        "Dissolved magnesium makes water harder.",
      "</p>",
      "<p>",
        "European 2009/54/EC Directive classifies mineral waters with more than 50 mg/L of magnesium as &lsquo;water with magnesium&rsquo;,",
        "and they classify mineral waters with more than 200 mg/L of sulfate as 'water with sulphate'.",
        "This chemical has been discovered on the planet Mars and the dwarf planet Ceres.",
      "</p>"
    ],
    "fullName": "Magnesium Sulfate",
    "id": "MgSO4",
    "shortDescription": "This is a short description."
  },

  {
    "altNames": [ "Baking soda", "bicarbonate of soda", "nahcolite" ],
    "formula": "NaHCO₃",
    "fullDescription": [
      "Raises pH (makes water more alkaline), which in moderation helps with exfoliation and also makes skin feel smoother.",
      "Water with pH between 7.5 and 8.1 can provide these benefits.",
      "European 2009/54/EC Directive classifies mineral waters with more than 200 mg/L of sodium, as &lsquo;water with sodium&rsquo;."
    ],
    "fullName": "Sodium Bicarbonate",
    "id": "NaHCO3",
    "shortDescription": "This is a short description."
  },

  {
    "altNames": [ "Vitamin C", "ascorbic acid", "sodium salt of ascorbic acid", "food additive E301" ],
    "formula": "C₆H₇O₆Na",
    "fullDescription": [
      "Biofriendly and safe dechlorinator.",
      "Food additive E301.",
      "An antioxidant and acidity regulator."
    ],
    "fullName": "Sodium Ascorbate",
    "id": "C6H7NaO6",
    "shortDescription": "Biofriendly and safe dechlorinator."
  },

  {
    "altNames": [ "Glauber&rsquo;s salt", "sodium sulphate", "food additive E514", "decahydrate of sodium sulfate" ],
    "formula": "Na₂SO₄",
    "fullDescription": [
      "The <a href='https://www.onsen-msrc.com/kenko_f/onsen_english/contents/donoonsen.html'>Onsen Medical Scient Research Center</a> in Japan",
      "advocates bathing in hot springs (Onsen) containing sodium sulfate for high blood pressure, hardening of the arteries, and external wounds.",
      "Sodium sulfate is used by the food industry as a diluent for food colors.",
      "Its largest use is as filler in powdered home laundry detergents.",
      "It is a neutral salt, which forms aqueous solutions with pH of 7.",
      "European 2009/54/EC Directive classifies mineral waters with more than 200 mg/L of sulfate as &lsquo;water with sulphate&rsquo;.",
      "Pure sodium sulfate dust can cause temporary asthma or eye irritation; do not pour into water from a height."
    ],
    "fullName": "Sodium Sulfate",
    "id": "Na2SO4",
    "shortDescription": "This is a short description."
  },

  {
    "altNames": [ "Salt", "table salt", "pool salt" ],
    "formula": "NaCl",
    "fullDescription": [
      "European 2009/54/EC Directive classifies mineral waters with more than 200 mg/L of chloride, as &lsquo;water with chloride&rsquo;."
    ],
    "fullName": "Sodium Chloride",
    "id": "NaCl",
    "shortDescription": "This is a short description."
  },
]

Colors

colors.json
[
  {
    "kidFriendly": False,
    "id": "natural",
    "name": "Natural (mostly or completely white)"
  },
  {
    "kidFriendly": False,
    "id": "soft_red",
    "name": "Soft red"
  },
  {
    "kidFriendly": False,
    "id": "soft_green",
    "name": "Soft green"
  },
  {
    "kidFriendly": False,
    "id": "soft_blue",
    "name": "Soft blue"
  },
  {
    "kidFriendly": False,
    "id": "soft_orange",
    "name": "Soft orange"
  }
]

Essential Oils

For the video demo, there was no photo_toxic property. This will change for the real product.

essential_oils.json
[
  {
    "kidFriendly": False,
    "id": "bergamot",
    "longDescription": "Not photo-toxic.",
    "name": "Bergamot - Bergaptene Free",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "cedarwood",
    "longDescription": "",
    "name": "Cedarwood - Atlas",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "frankincense",
    "longDescription": "",
    "name": "Frankincense - India",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "ginger_root",
    "longDescription": "",
    "name": "Ginger Root",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": True,
    "id": "lavender",
    "longDescription": "",
    "name": "Lavender - natural blend",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "lemon",
    "longDescription": "This is a long description",
    "name": "Lemon - natural blend",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "lemongrass",
    "longDescription": "This is a long description",
    "name": "Lemongrass",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "orange",
    "longDescription": "This is a long description",
    "name": "orange - natural blend",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "spearmint",
    "longDescription": "This is a long description",
    "name": "Spearmint",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "tea_tree",
    "longDescription": "This is a long description",
    "name": "Tea tree - organic",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "turmeric",
    "longDescription": "This is a long description",
    "name": "Turmeric - Organic",
    "shortDescription": "This is a short description"
  },
  {
    "kidFriendly": False,
    "id": "ylang_ylang",
    "longDescription": "This is a long description",
    "name": "Ylang Ylang",
    "shortDescription": "This is a short description"
  }
]

Mixtures

For the video demo, mixtures were called recipes.

mixtures.json
[
  {
    "created_on": "2021-01-01",
    "description": "Enjoy a hot springs experience in the comfort of your own bathtub!",
    "ingredients": [
      {
        "id": "KCl",
        "parts": 10
      },
      {
        "id": "NaCl",
        "parts": 10
      },
      {
        "id": "NaHCO3",
        "parts": 2
      }
    ],
    "essential_oils": [ ],
    "id": "1",
    "name": "Hot Spring Mix #1",
    "user_id": "Original"
  },
  {
    "created_on": "2021-01-20",
    "description": "Unwind after a long week.",
    "ingredients": [
      {
        "id": "KCl",
        "parts": 10
      },
      {
        "id": "NaCl",
        "parts": 10
      },
      {
        "id": "NaHCO3",
        "parts": 4
      }
    ],
    "essential_oils": [
      { "lemongrass": 5 }
    ],
    "id": "2",
    "name": "Relaxation Mix #1",
    "user_id": "Original"
  },
  {
    "created_on": "2021-02-01",
    "description": "Rejuvenate body and soul.",
    "ingredients": [
      {
        "id": "C6H7NaO6",
        "parts": 1
      },
      {
        "id": "KCl",
        "parts": 20
      },
      {
        "id": "MgCl",
        "parts": 20
      },
      {
        "id": "NaCl",
        "parts": 20
      },
      {
        "id": "NaHCO3",
        "parts": 5
      },
      {
        "id": "Na2SO4",
        "parts": 3
      }
    ],
    "essential_oils": [
      { "lemongrass": 5 },
      { "lavender": 5 },
      { "frankincense": 1 }
    ],
    "id": "3",
    "name": "Rejuvenation Mix #1",
    "user_id": "Original"
  }
]

JavaScript

Used for proof of concept video demo. Since then, the term recipe has been superceded by the term mixture.

order.js
---
---
class Chemical {
  constructor({altNames, formula, fullDescription, fullName, id, shortDescription}) {
    this.altNames = altNames;
    this.formula = formula;
    this.fullDescription = fullDescription;
    this.fullName = fullName;
    this.id = id;
    this.shortDescription = shortDescription;
  }
}

class Chemicals {
  constructor(chemicalsJsonArray) {
    this.chemicals = chemicalsJsonArray.map(chemicalJson => {
      return new Chemical(chemicalJson);
    });
  }
}

class EssentialOil {
  constructor({name, parts}) {
    this.name = name;
    this.parts = parts;
  }
}

class Ingredient {
  constructor({id, parts}) {
    this.id = id;
    this.parts = parts;
  }

  asHtml() {
    return `<li>${this.id}<span class='recipe_gallery_part'>: ${this.parts} parts</span></li>`;
  }
}

class Recipe {
  constructor({
    color = "none",
    color_material = "none",
    created_on = "",
    description = "",
    essential_oils = [],
    id,
    ingredients,
    name,
    user_id
  }) {
    this.color = color;
    this.color_material = color_material;
    this.created_on = created_on;
    this.description = description;
    this.essential_oils = essential_oils;
    this.id = id;
    this.ingredients = ingredients;
    this.name = name;
    this.user_id = user_id;
  }

  asHtml() {
    var description = "";
    if (this.description.length>0)
      description = `<div class='recipe_description'>${ this.description }</div>`;

    var ingredients = this.ingredients.map(ingredient => {
      return new Ingredient(ingredient).asHtml();
    }).join("");

    return `<div class='recipe' data-recipeId="${this.id}">
      <div class='recipe_user_id'>${this.user_id}</div>
      <div class='recipe_title'>${this.name}</div>
      ${description}
      <ul class="recipe_ingredients recipe_gallery_ingredient_summary">${ingredients}</ul>
    </div>`;
  }
}

class Recipies {
  constructor (recipiesJsonArray) {
    this.recipies = recipiesJsonArray.map(recipeJson => {
      return new Recipe(recipeJson);
    });
  }

  nextId() {
    return (this.recipies.length + 1).toString();
  }
}


const chemicals = new Chemicals({% flexible_include file='order/chemicals.json' do_not_escape='true' %});

const recipies = new Recipies({% flexible_include file='order/mixtures.json' do_not_escape='true' %});

var infoForChemicalId = "";  // Keep track of chemical info displayed in modal dialog
var incremented = false;     // Has the recipe name/number been incremented yet?
var showRecipeGalleryParts = false;
var recipeName = "";         // Currently edited recipe name
var userId = "";             // Logged in userId


/* @return object only containing key/value entries of interest */
function ownProperties(oldObject) {
  var newObject = {};
  Object.getOwnPropertyNames(oldObject).forEach ( key => {
    // console.log(`key=${key}; value=${oldObject[key]}`)
    newObject[key] = oldObject[key];
  });
  return newObject;
}

/* @return array of string values extracted from innerText of selector results */
function jquerySelectorNodes(selector) {
  var valueNodes = $(selector);
  var result = [];
  for (const name in valueNodes) {
    i = parseInt(name);
    if (name.length>0 && i == i) // NaN is never equal to itself
      result.push(valueNodes[name]);
  }
  return result;
}

function chemicalIdsAvailable() {
  var result = chemicalIdsDefined().filter(x => { return !chemicalIdsUsed().includes(x) });
  // console.log(`chemicalIdsAvailable(): ${result.join(", ")}`);
  return result;
}

function chemicalIdsDefined() {
  var result = [];
  chemicals.chemicals.forEach(item => {
    result.push(item.id);
  });
  // console.log(`chemicalIdsDefined(): ${result.join(", ")}`);
  return result;
}

/* @return array of string containing chemical ids */
function chemicalIdsUsed() {
  var result = [];
  jquerySelectorNodes('#tbody [data-chemicalId]').forEach(trNode => {
    var item = trNode.getAttribute("data-chemicalId");
    result.push(item);
  });
  // console.log(`chemicalIdsUsed(): ${result.join(", ")}`);
  return result;
}

/* Compute grams for each ingredient from percentage and set tag text to those values */
function computeGrams(totalWeight, percentArray) {
  var grams = $('.grams');
  for (i=0; i < percentArray.length; i++) {
    grams[i].textContent = Math.round(percentArray[i] / 100.0 * totalWeight);
  }
}

/* Compute percentages for each ingredient from part and set tag text to those values
 * @return array of floats containing percentages of each ingredient */
function computePercentages(partArray) {
  var totalParts = partArray.reduce(function(a, b) {
      return a + b;
  }, 0);
  var percentages = $('.percentage');
  var percentArray = [];
  for (i=0; i < partArray.length; i++) {
    percentage = Math.round(100.0 * partArray[i] / totalParts);
    percentArray.push(percentage);
    percentages[i].textContent = `${percentage}%`;
  }
  return percentArray;
}

/* Compute total parts for all chemicals and set tag text to that values
 * @return array of floats containing parts of each ingredient */
function computeParts() {
  var partArray = [];
  var sum = 0.0;
  jquerySelectorNodes('[contenteditable="true"]').forEach(node => {
    value = parseFloat(node.innerText);
    if (value == value) {
      partArray.push(value);
      sum += value;
    }
  });
  $('#total_parts').text(sum);
  return partArray;
}

/* Compute parts, percentages and grams.
 * Side effect: sets tag text for percentages and grams, and totals. */
function computeTotals(totalWeight) {
  var partArray = computeParts();
  var percentArray = computePercentages(partArray);
  computeGrams(totalWeight, percentArray);
}

/* Computes total desired weight from user bathtub and reason, sets that value in page, displays weight tag.
* @return total weight in grams (float). */
function computeWeight() {
  var bathtub_gallons = $("#bathtub_gallons").val();
  var reason = $("#reason").val();

  var factor = 5;
  if (reason == "light")
    factor = 10;
  else if (reason == "medium")
    factor = 15;
  else if (reason == "intense")
    factor = 20;

  totalWeight = Math.round(bathtub_gallons * factor);
  $("#total_weight").text(totalWeight);
  $("#total_grams").text(totalWeight);
  $("weight_tag").show();
  return totalWeight;
}

/* Computes weights and totals, displays addRow button if any chemicals remain to be chosen. */
function computeEverything() {
  computeTotals(computeWeight());

  if (chemicalIdsAvailable().length>0)
    $(".addRow").show();
  else
    $(".addRow").hide();
}

function createChemicalCheckboxes() {
  var chemicalsJsonArray = [];
  chemicalIdsAvailable().forEach( id => {
    chemicalsJsonArray.push(findChemicalById(id));
  });

  //console.log(`chemicals: ${chemicals}`);
  $("#chemicalList")
    .empty()
    .append('<tr id="lastrow">');

  chemicalsJsonArray.forEach(chemicalJson => { insertChemicalRow(chemicalJson) });
}

function dirtyHandler(isDirty) {
  if (isDirty) {
    $("#recipe_name").css("border", "dotted thin grey");
    $("#recipe_name").css("color", "#286090");
    $("#save").css('visibility', 'visible');
  } else {
    $("#recipe_name").css("border", "inherit");
    $("#recipe_name").css("color", "inherit");
    $("#save").css('visibility', 'hidden');
  }
}

/* @return JSON object for chemical with given id */
function findChemicalById(id) {
  return chemicals.chemicals.filter(obj => {
    return obj.id === id;
  })[0];
}

function addChemicalToRecipe(chemicalJson, parts) {
  var newRow = `<tr data-chemicalId="${chemicalJson.id}">
      <td class="pt-3-half noselect">${chemicalJson.fullName}</td>
      <td class="pt-3-half numeric editable" contenteditable="true">${parts}</td>
      <td class="pt-3-half numeric noselect percentage"></td>
      <td class="pt-3-half numeric noselect grams"></td>
      <td class="minimal">
        <button type="button" class="cellButton deleteRow">
          <span class="fas fa-minus red" alt="Delete this ingredient from the recipe" title="Delete this ingredient from the recipe"></span>
        </button>
      </td>
    </tr>`;
  $("#tbody").prepend(newRow);
}

/** Create single string from array of strings in chemical.fullDescription */
function fullDescription(chemical) {
  return chemical.fullDescription.join(" ");
}

/* Delete ingredient row when user clicks on icon */
function handleDeleteRowClick(event) {
  tr = event.target.parentElement.parentElement.parentElement;
  tbody = tr.parentElement;
  tr.remove();
  computeEverything();

  if (tbody.rows.length == 1) // Remember there is a dummy row at the end
    $("#total_grams").text(0);
}

/* Update computations whenever an input or selection changes */
function handleInputChanges(event) {
  if (isNumeric(event)) {
    $("#recipe_name").attr('contenteditable', 'true');
    incrementSuffix('#recipe_name');
    computeEverything();
    dirtyHandler(true);
  } else if (event.target.id == "recipe_name" || event.target.id == "recipe_description") {
    dirtyHandler(true);
  }
}

/* Handle clicks on checkbox info icons in modal dialog */
function handleModalInfoIconClicks(event) {
  var chemicalId = event.target.getAttribute("data-chemicalId");
  var chemical = findChemicalById(chemicalId);
  $("#chemicalAbout").text(chemical.fullName);
  $("#chemicalAKA").html(chemical.altNames.join(", "));
  $("#chemicalDescription").html(chemical.fullDescription.join(" "));

  var chemicalInfoDiv = $("#chemicalInfoDiv");
  if (infoForChemicalId==chemicalId) {
    chemicalInfoDiv.toggle("slow");
  } else {
    chemicalInfoDiv.show("slow");
  }
  infoForChemicalId = chemicalId;

  event.preventDefault();
}

/* Handle change of bathtub capacity */
function handleBathtubResize(event) {
  computeEverything();
}

/* Handle close of chemical modal dialog */
function handleChemicalModalClose(event, modal) {
  var checkedBoxes = $("#chemicalList :checked");
  for (const name in checkedBoxes) {
    i = parseInt(name);
    if (name.length>0 && i == i) { // NaN is never equal to itself
      var chemicalId = checkedBoxes[i].value;
      var chemical = findChemicalById(chemicalId);
      addChemicalToRecipe(chemical);
      computeEverything();
    }
  }
}

/* Handle setup of chemical add modal dialog */
function handleChemicalModalSetup(event) {
  createChemicalCheckboxes();
  $('#addChemical').modal();
}

function handleChooseRecipe(event) {
  var recipe_gallery = $('#recipe_gallery');
  recipe_gallery.empty();
  recipies.recipies.forEach(recipe =>{
    recipe_gallery.append(recipe.asHtml());
  });
  if (showRecipeGalleryParts) {
    $("#show_parts").prop('checked', true);
    $(".recipe_gallery_part").show();
  }
  $('#recipies').modal();
}

function handleLogin(event) {
  event.preventDefault();
  userId = $("#userId").val();
  password = $("#password").val();
  if (userId.length>2) { // fake a login
    setUserId(userId);
    $("#recipe_name").addClass("editable");
    handleChemicalModalSetup("ignored");
  }
}

/* Only allow numbers in numeric fields */
function handleNumericKeypress(event) {
  if (!isNumeric(event)) {
    event.preventDefault();
    event.stopPropagation();
  }
}

/* Only allow numbers in numeric fields */
function handleTextKeypress(event) {
  if (!isText(event)) {
    event.preventDefault();
    event.stopPropagation();
  }
}

/* Handle change of reason for wanting bath salts */
function handleReasonChange(event) {
  computeEverything();
}

function zipIdParts(ids, parts) {
  return ids.map((id, i) => {
    return { id: id, parts: parts[i] };
  });
}

function handleLoginClose(event) {
  $("#recipe_author").text(userId);
  var parts = jquerySelectorNodes('#tbody [contenteditable="true"]')
    .map(node => {
      return parseFloat(node.innerText);
    });

  var newRecipe = new Recipe({
    "created_on": new Date().toISOString().split('T')[0],
    "description": $("#recipe_description").text(),
    "ingredients": zipIdParts(chemicalIdsUsed(), parts),
    "essential_oils": [ ],
    "id": recipies.nextId(),
    "name": recipeName,
    "user_id": userId
  });
  recipies.recipies.push(newRecipe);
}

function handleSave(event) {
  recipeName = $("#recipe_name").text();
  if (userId.length==0) {
    $('#login').modal();
    $("#userId").focus();
    dirtyHandler(false);
  } else {
    dirtyHandler(false);
    handleLoginClose();
  }
}

function handleSelectedRecipe(event) {
  var recipeId = event.target.parentElement.getAttribute("data-recipeId");
  loadRecipe(recipeId);
  $.modal.close();
}

function handleShowRecipeGalleryParts(event) {
  showRecipeGalleryParts = !showRecipeGalleryParts;
  if (showRecipeGalleryParts)
    $(".recipe_ingredients").removeClass("recipe_gallery_ingredient_summary");
  else
    $(".recipe_ingredients").addClass("recipe_gallery_ingredient_summary");
  $(".recipe_gallery_part").toggle();
}

function incrementSuffix(selector) {
  if (incremented)
    return;
  var value = $(selector).text();
  value = value.replace(/(\d+)$/, function (match, n) {
      return ++n;
  });
  $(selector).text(value);
  incremented = true;
}

function insertChemicalRow(chemicalJson) {
  $("#chemicalList")
    .append($("<label>")
      .text(`${chemicalJson.fullName} (${chemicalJson.formula})`)
      .css("display", "block")
      .append($(`<span class='fas fa-info-circle chemicalCheckbox' data-chemicalId='${chemicalJson.id}'></span>`))
      .prepend($(`<input type='checkbox'>`)
        .val(chemicalJson.id)
      )
    );
}

function isNumeric(event) {
  var x = event.charCode || event.keyCode;
  return !(isNaN(String.fromCharCode(event.which)) && x!=46 || x===32 || x===13 || (x===46 && event.currentTarget.innerText.includes('.')));
}

function isText(event) {
  var key = event.key;
  var code = event.code;
  var which = event.which;
  // console.log(`code=${code}; key=${key}; which=${which}`);
  valid = which >= 32 && (
    key.match(/^[0-9a-zA-Z]+$/) ||
    ' -_\'"()[]{}+-#%'.includes(key)
  );
  return valid;
}

function loadRecipe(id) {
  $("#tbody")
    .empty()
    .append('<tr id="lastrow">');

  var recipe = recipies.recipies.find(x => x.id === id.toString());
  $("#recipe_author").text(recipe.user_id);
  $("#recipe_name").text(recipe.name);
  $("#recipe_created_on").text(recipe.created_on);
  $("#recipe_description").text(recipe.description);
  recipe.ingredients.forEach(ingredientsJson => {
    var chemicalJson = chemicals.chemicals.find(x => x.id === ingredientsJson.id);
    addChemicalToRecipe(chemicalJson, ingredientsJson.parts);
  });
  // do the same for essential oils
  // do the same for color
  computeEverything();
}

function setUserId(newUserId) {
  if (userId.length == 0)
    $("#recipe_author").text(newUserId);
}


window.onload = function() {
  // Delegated event handlers for generated tags
  $(document).on('keypress', '.numeric.editable',        handleNumericKeypress);
  $(document).on('keypress', '.text.editable',           handleTextKeypress);
  $(document).on('keyup',    '[contenteditable="true"]', handleInputChanges);
  $(document).on('click',    '.deleteRow',               handleDeleteRowClick);
  $(document).on('click',    '.chemicalCheckbox',        handleModalInfoIconClicks);
  $(document).on('click',    '#login',                   handleLogin);
  $(document).on('click',    '#show_parts',              handleShowRecipeGalleryParts);
  $(document).on('click',    '.recipe',                  handleSelectedRecipe);

  // Normal event handlers for static tags
  $('.addRow').click(handleChemicalModalSetup);
  $('#addChemical').on($.modal.BEFORE_CLOSE, handleChemicalModalClose);
  $('#login').on($.modal.BEFORE_CLOSE, handleLoginClose);
  $('#bathtub_gallons').keyup(handleBathtubResize);
  $('#chooseRecipe').click(handleChooseRecipe);
  $('#reason').change(handleReasonChange);
  $('#save').click(handleSave);

  // Initialization
  loadRecipe(1);
  computeEverything();
};

pricing Django App

Refer to my initial django-oscar notes.

models.py
from datetime import datetime
from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils import timezone
from typing import *  # noqa
from bather.models import Bather
from oscar.apps.order.models import Order as OscarOrder

class Classification(models.Model):
    name = models.TextField()
    parent = models.ForeignKey('self', null=True, on_delete=models.CASCADE)
    """See https://docs.djangoproject.com/en/3.1/ref/models/fields/#django.db.models.ForeignKey"""

    def __str__(self) -> str:
        return self.name

class TimeUnits():
    YEAR: Final = "year"
    MONTH: Final = "month"
    WEEK: Final = "week"
    DAY: Final = "day"
    HOUR: Final = "hour"
    MINUTE: Final = "minute"
    SECOND: Final = "second"

class Chemical(models.Model):
    alt_names = models.TextField(null=True)
    """ Contains JSON array of string """

    full_description = models.TextField()
    name = models.TextField()

    private_comment = models.TextField(null=True)
    """Only Ancient Warmth users will see this information"""

    recommended_max_percent = models.PositiveIntegerField(default=100, validators=[MaxValueValidator(100)])
    short_description = models.TextField()

    remaining_inventory = models.PositiveIntegerField(default=0)
    """grams"""

    retail_price_per_kg = models.PositiveIntegerField(null=True)
    """Cents.
    Recomputed each time we pay for a new shipment.
    If null, Designs that require this Chemical cannot be sold.
    System should call dependant_designs() to warn any Ancient Warmth staff user who tries to set this field to null that
    "xxx Designs rely on this Chemical, and they will become unavailable".
    """

    @property
    def designs(self) -> List["Design"]:
        """@return list of Designs that require this Chemical"""
        pass

    @property
    def mixtures(self)-> List["Mixture"]:
        """@return list of Mixtures that require this Chemical"""
        pass

    @property
    def bathers(self)-> List["Bather"]:
        """@return list of Bathers that require this Chemical"""
        pass

    @property
    def designers(self)-> List["Designer"]:
        """@return list of Designers that require this Chemical"""
        pass

    @property
    def usage_rate(self, time_unit=TimeUnits.YEAR, n:int=12) -> int:
        """@return grams used per unit time, averaged over the most recent n time units"""
        pass

    @property
    def usage_rate_history(self, time_unit=TimeUnits.MONTH, max_length: int = 12) -> List[int]:
        """
        @return list of grams used per unit time
        """
        pass

    def __str__(self):
        return self.name

class Color(models.Model):
    kid_friendly = models.BooleanField()
    name = models.TextField()

    def __str__(self):
        return self.name

class Mixture(models.Model):
    """
    A Mixture's ingredients cannot be modified or deleted, however all other properties can be modified by Ancient Warmth and the Designer.
    This ensures that Bathers will be able to trust Designs, which are partially based on Mixtures.
    If a Designer wants a different formulation they can create a new Mixture version.

    The components of a Mixture are measured in percentage of the total grams of that component per the total grams.

    Many fields of a Mixture are immutable.
    """

    readonly_fields=('created', 'designer', 'parent', 'version')

    classification = models.ForeignKey(Classification, null=True, on_delete=models.CASCADE)

    created = models.DateTimeField(verbose_name='Date/time this mixture was defined', auto_now_add=True)
    description = models.TextField(null=True)
    designer = models.OneToOneField('Designer', null=True, on_delete=models.CASCADE)
    """
    If null, then this is an Original mixture (defined by Ancient Warmth).
    """

    last_modified = models.DateTimeField(verbose_name='Date/time this mixture was modified', auto_now=True)
    """The chemicals in this mixture can never be changed, only other properties such as classification, description or tags"""

    name = models.TextField()
    """Does not include the version number"""

    parent = models.OneToOneField('self', null=True, on_delete=models.CASCADE)
    """
    Mixture that this mixture was copied from.
    If null, then the parent is an Original mixture (defined by Ancient Warmth).
    """

    skin = models.ForeignKey('DesignSkin', null=True, on_delete=models.CASCADE)
    """
    Provides default value for Design.skin based on this mixture.
    TODO decide where/how the default skin is defined.
    """

    version = models.PositiveIntegerField(default=1)

    @property
    def bathers(self)-> List[Bather]:
        """@return list of Bathers that have used this Mixture"""
        pass

    @property
    def designers(self) -> List["Designer"]:
        """@return list of Designers that have used this Mixture in their Designs"""
        pass

    @property
    def designs(self) -> List["Design"]:
        """@return list of Designs that use this Mixture"""
        pass

    @property
    def full_heritage(self) -> List['Mixture']:
        """Return list of parents"""
        pass

    @property
    def full_name(self) -> str:
        return f'{self.designer.user.handle}/{self.name} #{self.version}'

    @property
    def heritage(self) -> str:
        return f"Copied from {self.parent.full_name}, originated from {self.originator.full_name})"

    @property
    def originator():
        pass #  models.OneToOneField('self', null=True, on_delete=models.CASCADE)
    """
    Mixture that this mixture was originally copied from (top of parent hierarchy).
    If null, then the original mixture is an Original mixture (defined by Ancient Warmth).
    """

    @property
    def usage_rate(self, time_unit=TimeUnits.YEAR, n:int=12) -> int:
        """@return grams used per unit time, averaged over the most recent n time units"""
        pass

    def __str__(self):
        return self.name

class Design(models.Model):
    """
    A Design's ingredients cannot be modified or deleted, however all other properties can be modified by Ancient Warmth and the Designer.
    This ensures that Bathers will be able to trust Designs.
    If a Designer wants a different formulation they can create a new Design version.

    Designs scale up and down according to the bathtub capacity, they are independent of the size of the bathtub.

    Ranking will be computed by another class.

    A Design can only be defined after its mixture is defined.

    Many fields are immutable.
    """

    readonly_fields=('color', 'created', 'designer', 'mixture', 'mixture_intensity', 'parent', 'version')

    color = models.OneToOneField(Color, null=True, on_delete=models.SET_NULL)
    designer = models.OneToOneField('Designer', null=True, on_delete=models.CASCADE)
    """
    If null, then this is an Original mixture (defined by Ancient Warmth).
    """

    created = models.DateTimeField(verbose_name='date/time created', auto_now_add=True)
    description = models.TextField(null=True)

    last_modified = models.DateTimeField(verbose_name='date/time modified', auto_now=True)
    """
    A Design's mixture and mixture_grams cannot be modified or deleted.
    This ensures that Bathers will be able to trust the Design.
    If a Designer wants a different formulation they can create a new Design version.

    Matches by mixture.id, not mixture.name; this allows mixtures to be renamed without breaking the association with Designs.
    """

    mixture = models.OneToOneField(Mixture, null=True, on_delete=models.SET_NULL)
    mixture_intensity = models.FloatField(default=1.0, validators=[MinValueValidator(0.1), MaxValueValidator(5)])
    """
    Multiplier for mixture ingredients. Typical values:
      1.0 - recreational
      2.0 - light therapeutic
      3.0 - medium therapeutic
      4.0 - intense therapeutic
    """

    name = models.TextField()
    """Does not include the version number"""

    parent = models.OneToOneField('self', null=True, on_delete=models.CASCADE)
    """
    Design that this design was copied from.
    If null, then the parent is an Original design (defined by Ancient Warmth).
    """

    skin = models.ForeignKey('DesignSkin', null=True, on_delete=models.CASCADE) # FIXME should have something like default=mixture.skin)
    """
    There is no restriction on which Designer's skin this Design uses.
    """

    version = models.PositiveIntegerField(default=1)

    @property
    def bathers(self)-> List['Bather']:
        """@return list of Bathers that have used this Design"""
        pass

    @property
    def designers(self)-> List["Designer"]:
        """@return list of Designers that have cloned this Design, plus the original Designer who created it"""
        pass

    @property
    def full_heritage(self) -> List['Mixture']:
        """Return list of parents"""
        pass

    @property
    def full_name(self) -> str:
        return f'{self.designer.user.handle}/{self.name} #{self.version}'

    @property
    def heritage(self) -> str:
        return f"Copied from {self.parent.full_name}, originated from {self.originator.full_name})"

    @property
    def mixture_grams_per_liter(self):
        """
        Calibrated for recreational use (200 grams of mixture for 40 US gallons)
        There are about 150 liters in 40 US gallons
        """
        self.mixture_intensity * 200.0 / 150.0

    @property
    def originator(self):
        pass #  models.OneToOneField('self', null=True, on_delete=models.CASCADE)
    """
    Design that this design was originally copied from (top of parent hierarchy).
    If null, then the original design is an Original design (defined by Ancient Warmth).
    """

    def usage_rate(self, time_unit=TimeUnits.YEAR, n:int=12) -> int:
        """@return grams used per unit time, averaged over the most recent n time units"""
        pass

    def __str__(self):
        return self.name

    def mixture_grams_for(self, bathtub_liters: float) -> int:
        return int(self.bathtub_liters * self.mixture_grams_per_liter)

    def oil_ml_for(self, bathtub_liters: float, oil_mg_per_liter: float) -> int:
        """
        Calibrated for recreational use (200 grams of mixture for 40 US gallons).
        There are about 150 liters in 40 US gallons.
        """
        return int(self.bathtub_liters * self.oil_mg_per_liter)

class Designer(models.Model):
    """
    A Design is a Mixture, customized by adding items that do not affect the chemistry, but do affect the bather’s experience.
    The components of a Design are measured in mg or mL per liter of bathtub water.
    """
    mission_statement = models.TextField(null=True)
    user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)

    def __str__(self):
        return self.user.name

class DesignSkin(models.Model):
    readonly_fields=('created', 'designer')

    created = models.DateTimeField(verbose_name='Date/time created', auto_now_add=True)
    description = models.TextField(null=True)
    designer = models.ForeignKey('Designer', null=True, on_delete=models.CASCADE)
    graphics = models.ImageField()
    last_modified = models.DateTimeField(verbose_name='Date/time last modified', auto_now=True)
    name = models.TextField()

class EssentialOil(models.Model):
    kid_friendly = models.BooleanField(default=False)
    full_description = models.TextField()
    name = models.TextField()

    private_comment = models.TextField(null=True)
    """Only Ancient Warmth users will see this information"""

    recommended_max_ml_per_liter = models.FloatField(default=0.2, validators=[MinValueValidator(0), MaxValueValidator(1)])
    """TODO read this from a config file."""

    retail_price_per_ml = models.PositiveIntegerField(default=150)
    """
    Cents.
    Reset each time we pay for a new shipment.
    TODO read this from a config file.
    """

    remaining_inventory_ml = models.PositiveIntegerField(default=0)
    short_description = models.TextField()

    @property
    def bathers(self)-> List["Bather"]:
        """@return list of Bathers that have used a package containing this EssentialOil"""
        pass

    @property
    def designs(self) -> List["Design"]:
        """@return list of Designs that require this EssentialOil"""
        pass

    @property
    def designers(self) -> List["Designer"]:
        """@return list of Designers that have created or clones Designs that require this EssentialOil"""
        pass

    @property
    def usage_rate(self, time_unit=TimeUnits.YEAR, n:int=12) -> int:
        """@return grams used per unit time, averaged over the most recent n time units"""
        pass

    def __str__(self):
        return self.name

class Scent(EssentialOil):
    """No changes can be made to a Scent (it is immutable) once it is created"""

    readonly_fields=('essential_oil', 'design', 'ml_per_liter')

    design = models.ForeignKey(Design, null=True, on_delete=models.CASCADE)
    ml_per_liter = models.PositiveIntegerField()

    def __str__(self):
        return f"{self.ml_per_liter} of {self.essential_oil}"

class Ingredient(Chemical):
    """No changes can be made to an ingredient (it is immutable) after it is defined"""

    readonly_fields=('chemical', 'mixture', 'parts')

    mixture = models.ForeignKey(Mixture, null=True, on_delete=models.CASCADE)
    parts = models.PositiveIntegerField()

    def __str__(self):
        return f"{self.parts} of {self.chemical}"

class Inventory(models.Model):
    """Raw materials inventory"""
    # TODO Write me
    pass

class Package(Design):
    """The only Package field that can be modified is price, all other fields are immutable."""

    readonly_fields=('bather', 'design', 'grams')

    bather = models.ForeignKey(Bather, null=True, on_delete=models.CASCADE)
    grams = models.PositiveIntegerField(default=200, validators=[MinValueValidator(50), MaxValueValidator(10 * 1000)])

    @property
    def price(self):
        # TODO write me
        pass

    @property
    def name(self):
        return f"{self.design.name}, {self.grams} grams"

    def __str__(self):
        return f"{self.name} by {self.design} for {self.bather.full_name}"

    # class Meta:
        # ordering = ['self.design.category', 'self.design.name']

class Rating(models.Model):
    """The only Rating field that can be modified is value"""

    readonly_fields=('bather', 'design')

    bather = models.ForeignKey(Bather, null=True, on_delete=models.CASCADE)
    design = models.ForeignKey(Design, null=True, on_delete=models.CASCADE)
    value = models.PositiveIntegerField(default=3, validators=[MinValueValidator(0), MaxValueValidator(5)])

    def __str__(self):
        return f"{self.value} stars for {self.design} by {self.bather}"

class Tub(models.Model):
    """Some bathers have a more than one bathtub, plus maybe a hot tub"""

    readonly_fields=('bather')

    bather = models.ForeignKey(Bather, null=True, on_delete=models.CASCADE)

    capacity_us_gallons = models.PositiveIntegerField(default=40)

    name = models.TextField(default="bathtub")

    @property
    def capacity_liters(self) -> float:
        return self.capacity_us_gallons * 3.78541

    def __str__(self):
        return f"{self.name}, {self.capacity_us_gallons} US gallons ({self.capacity_liters} liters)"

class Subscription(models.Model):
    """Does Oscar have this?"""

    created = models.DateTimeField(default=datetime.now)
    end_date = models.DateField(null=True)

    frequency = models.PositiveIntegerField(default=1)
    frequency_units = models.CharField(max_length=10)  # 'day', 'week', 'month', 'year'

    gift_wrap = models.BooleanField(default=False)
    gift_wrap_message = models.TextField(null=True)

    last_modified = models.DateTimeField(default=datetime.now)
    paused = models.BooleanField(default=False)
    start_date = models.DateField(null=False, default=timezone.now)

    def __str__(self):
        return f"Every {self.frequency} {self.frequency_units}s, from {self.start_date} to {self.end_date}; {'not ' if not self.paused else '' }paused."

class Order(models.Model):
    """Derived from Oscar's order."""

    gift_wrap = models.BooleanField(default=False)
    gift_wrap_message = models.TextField(null=True)

    order = models.ForeignKey(OscarOrder, null=True, on_delete=models.SET_NULL)
    subscription = models.ForeignKey(Subscription, null=True, on_delete=models.SET_NULL)

    def __str__(self):
        return f"{self.gift_wrap}"


if __name__ == '__main__':
    import json

    # Store the list in the database
    chemical = Chemical()
    chemical.name = "KCl"
    alt_names = ["Salt for diabetics", "reduced-sodium replacement salt", "NoSalt", "Nu-Salt", "No Salt Original"]
    chemical.alt_names = json.dumps(alt_names)
    # chemical.save()

    # Retrieve the list from the database
    jsonDecoder = json.decoder.JSONDecoder()
    alt_names = jsonDecoder.decode(chemical.alt_names)
Shell
$ ./manage.py makemigrations pricing
Migrations for 'pricing':
  pricing/migrations/0001_initial.py
    - Create model Bather
    - Create model Category
    - Create model Chemical
    - Create model Color
    - Create model Design
    - Create model Designer
    - Create model EssentialOil
    - Create model Scent
    - Create model Rating
    - Create model Package
    - Create model Mixture
    - Create model Ingredient
    - Add field designer to design
    - Add field mixture to design 
Shell
$ ./manage.py sqlmigrate pricing 0001
BEGIN;
--
-- Create model Bather
--
CREATE TABLE "aw_pricing_bather" ("user_id" integer NOT NULL PRIMARY KEY);
--
-- Create model Category
--
CREATE TABLE "aw_pricing_category" ("id" serial NOT NULL PRIMARY KEY, "name" text NOT NULL);
--
-- Create model Chemical
--
CREATE TABLE "aw_pricing_chemical" ("id" serial NOT NULL PRIMARY KEY, "alt_names" text NULL, "full_description" text NOT NULL, "name" text NOT NULL, "short_description" text NOT NULL);
--
-- Create model Color
--
CREATE TABLE "aw_pricing_color" ("id" serial NOT NULL PRIMARY KEY, "kid_friendly" boolean NOT NULL, "name" text NOT NULL);
--
-- Create model Design
--
CREATE TABLE "aw_pricing_design" ("id" serial NOT NULL PRIMARY KEY, "created" timestamp with time zone NOT NULL, "graphics" varchar(100) NOT NULL, "name" text NOT NULL, "category_id" integer NOT NULL UNIQUE, "color_id" integer NOT NULL UNIQUE);
--
-- Create model Designer
--
CREATE TABLE "aw_pricing_designer" ("user_id" integer NOT NULL PRIMARY KEY);
--
-- Create model EssentialOil
--
CREATE TABLE "aw_pricing_essentialoil" ("id" serial NOT NULL PRIMARY KEY, "kid_friendly" boolean NOT NULL, "full_description" text NOT NULL, "name" text NOT NULL, "short_description" text NOT NULL);
--
-- Create model Scent
--
CREATE TABLE "aw_pricing_scent" ("id" serial NOT NULL PRIMARY KEY, "ml_per_liter" integer NOT NULL CHECK ("ml_per_liter" >= 0), "design_id" integer NOT NULL, "essential_oil_id" integer NOT NULL UNIQUE);
--
-- Create model Rating
--
CREATE TABLE "aw_pricing_rating" ("id" serial NOT NULL PRIMARY KEY, "value" integer NOT NULL CHECK ("value" >= 0), "bather_id" integer NOT NULL, "design_id" integer NOT NULL);
--
-- Create model Package
--
CREATE TABLE "aw_pricing_package" ("id" serial NOT NULL PRIMARY KEY, "grams" integer NOT NULL CHECK ("grams" >= 0), "bather_id" integer NOT NULL, "design_id" integer NOT NULL UNIQUE);
--
-- Create model Mixture
--
CREATE TABLE "aw_pricing_mixture" ("id" serial NOT NULL PRIMARY KEY, "created" timestamp with time zone NOT NULL, "name" text NOT NULL, "designer_id" integer NOT NULL UNIQUE);
--
-- Create model Ingredient
--
CREATE TABLE "aw_pricing_ingredient" ("id" serial NOT NULL PRIMARY KEY, "parts" integer NOT NULL CHECK ("parts" >= 0), "chemical_id" integer NOT NULL UNIQUE, "mixture_id" integer NOT NULL);
--
-- Add field designer to design
--
ALTER TABLE "aw_pricing_design" ADD COLUMN "designer_id" integer NOT NULL CONSTRAINT "aw_pricing_design_designer_id_0be65681_fk_aw_pricin" REFERENCES "aw_pricing_designer"("user_id") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "aw_pricing_design_designer_id_0be65681_fk_aw_pricin" IMMEDIATE;
--
-- Add field mixture to design
--
ALTER TABLE "aw_pricing_design" ADD COLUMN "mixture_id" integer NOT NULL UNIQUE CONSTRAINT "aw_pricing_design_mixture_id_93091820_fk_aw_pricing_mixture_id" REFERENCES "aw_pricing_mixture"("id") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "aw_pricing_design_mixture_id_93091820_fk_aw_pricing_mixture_id" IMMEDIATE;
ALTER TABLE "aw_pricing_bather" ADD CONSTRAINT "aw_pricing_bather_user_id_6390db20_fk_auth_user_id" FOREIGN KEY ("user_id") REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_design" ADD CONSTRAINT "aw_pricing_design_category_id_c794e2d1_fk_aw_pricin" FOREIGN KEY ("category_id") REFERENCES "aw_pricing_category" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_design" ADD CONSTRAINT "aw_pricing_design_color_id_a8f9e074_fk_aw_pricing_color_id" FOREIGN KEY ("color_id") REFERENCES "aw_pricing_color" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_designer" ADD CONSTRAINT "aw_pricing_designer_user_id_8b9aabdf_fk_auth_user_id" FOREIGN KEY ("user_id") REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_scent" ADD CONSTRAINT "aw_pricing_scent_design_id_2ab15a89_fk_aw_pricing_design_id" FOREIGN KEY ("design_id") REFERENCES "aw_pricing_design" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_scent" ADD CONSTRAINT "aw_pricing_scent_essential_oil_id_700d88e4_fk_aw_pricin" FOREIGN KEY ("essential_oil_id") REFERENCES "aw_pricing_essentialoil" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "aw_pricing_scent_design_id_2ab15a89" ON "aw_pricing_scent" ("design_id");
ALTER TABLE "aw_pricing_rating" ADD CONSTRAINT "aw_pricing_rating_bather_id_c8957e24_fk_aw_pricin" FOREIGN KEY ("bather_id") REFERENCES "aw_pricing_bather" ("user_id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_rating" ADD CONSTRAINT "aw_pricing_rating_design_id_17651e3b_fk_aw_pricing_design_id" FOREIGN KEY ("design_id") REFERENCES "aw_pricing_design" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "aw_pricing_rating_bather_id_c8957e24" ON "aw_pricing_rating" ("bather_id");
CREATE INDEX "aw_pricing_rating_design_id_17651e3b" ON "aw_pricing_rating" ("design_id");
ALTER TABLE "aw_pricing_package" ADD CONSTRAINT "aw_pricing_package_bather_id_d0441e4f_fk_aw_pricin" FOREIGN KEY ("bather_id") REFERENCES "aw_pricing_bather" ("user_id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_package" ADD CONSTRAINT "aw_pricing_package_design_id_90fc01b7_fk_aw_pricing_design_id" FOREIGN KEY ("design_id") REFERENCES "aw_pricing_design" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "aw_pricing_package_bather_id_d0441e4f" ON "aw_pricing_package" ("bather_id");
ALTER TABLE "aw_pricing_mixture" ADD CONSTRAINT "aw_pricing_mixture_designer_id_be94523d_fk_aw_pricin" FOREIGN KEY ("designer_id") REFERENCES "aw_pricing_designer" ("user_id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_ingredient" ADD CONSTRAINT "aw_pricing_ingredien_chemical_id_2a2fd6ae_fk_aw_pricin" FOREIGN KEY ("chemical_id") REFERENCES "aw_pricing_chemical" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "aw_pricing_ingredient" ADD CONSTRAINT "aw_pricing_ingredien_mixture_id_cd5b0b60_fk_aw_pricin" FOREIGN KEY ("mixture_id") REFERENCES "aw_pricing_mixture" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "aw_pricing_ingredient_mixture_id_cd5b0b60" ON "aw_pricing_ingredient" ("mixture_id");
CREATE INDEX "aw_pricing_design_designer_id_0be65681" ON "aw_pricing_design" ("designer_id");
COMMIT; 
* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.