Reorganizing assets for game specific mods
parent
14fca5a001
commit
0968bfc157
|
@ -2,6 +2,7 @@
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
target/
|
target/
|
||||||
|
out/
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
|
4166
index.html
4166
index.html
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,164 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
enum Character {
|
||||||
|
#[serde(alias = "A")]
|
||||||
|
Aqua,
|
||||||
|
#[serde(alias = "V")]
|
||||||
|
Ventus,
|
||||||
|
#[serde(alias = "T")]
|
||||||
|
Terra,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct Ability {
|
||||||
|
name: String,
|
||||||
|
from: String,
|
||||||
|
category: String,
|
||||||
|
max: u8,
|
||||||
|
types: Vec<char>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Command {
|
||||||
|
name: String,
|
||||||
|
category: String,
|
||||||
|
char: Vec<char>,
|
||||||
|
#[serde(default)]
|
||||||
|
starting: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
info: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
recipes: Vec<CommandRecipe>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct CommandRecipe {
|
||||||
|
char: Vec<Character>,
|
||||||
|
r#type: char,
|
||||||
|
ingredients: (String, String),
|
||||||
|
chance: u8,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
abilities: Vec<(String, Ability)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRecipe {
|
||||||
|
pub fn get_ability(&self, crystal: &str) -> Option<Ability> {
|
||||||
|
if self.r#type == '-' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ability in self.abilities.iter() {
|
||||||
|
if ability.0 == crystal {
|
||||||
|
return Some(ability.1.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should never happen unless the json files are wrong
|
||||||
|
panic!(
|
||||||
|
"No ability found for {} + {} and {}",
|
||||||
|
self.ingredients.0, self.ingredients.1, crystal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_unlock(&self, char: Character) -> bool {
|
||||||
|
self.char.contains(&char)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_unlock_chars(&self) -> String {
|
||||||
|
let mut id = String::new();
|
||||||
|
if self.can_unlock(Character::Terra) {
|
||||||
|
id += "T"
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.can_unlock(Character::Ventus) {
|
||||||
|
if !id.is_empty() {
|
||||||
|
id += " ";
|
||||||
|
}
|
||||||
|
id += "V";
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.can_unlock(Character::Aqua) {
|
||||||
|
if !id.is_empty() {
|
||||||
|
id += " ";
|
||||||
|
}
|
||||||
|
id += "A";
|
||||||
|
}
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_abilities(&mut self, abilities: &[Ability]) {
|
||||||
|
let mut vec: Vec<(String, Ability)> = vec![];
|
||||||
|
|
||||||
|
for ability in abilities.iter() {
|
||||||
|
if ability.types.contains(&self.r#type) {
|
||||||
|
vec.push((ability.from.clone(), ability.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec.sort();
|
||||||
|
self.abilities = vec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Finisher {
|
||||||
|
name: String,
|
||||||
|
char: Vec<Character>,
|
||||||
|
level: u8,
|
||||||
|
#[serde(default)]
|
||||||
|
follows: Vec<String>,
|
||||||
|
goal: String,
|
||||||
|
color: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/bbs-commands.html", whitespace = "suppress")]
|
||||||
|
struct CommandsTemplate {
|
||||||
|
pub commands: Vec<Command>,
|
||||||
|
pub crystals: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ABILITIES_PATH: &str = "./input/bbs/abilities.json";
|
||||||
|
const FINISHERS_PATH: &str = "./input/bbs/finish-commands.json";
|
||||||
|
const COMMANDS_PATH: &str = "./input/bbs/commands.json";
|
||||||
|
|
||||||
|
pub fn init() {
|
||||||
|
tracing::info!("Loading abilities json data from {}", ABILITIES_PATH);
|
||||||
|
let abilities_str = std::fs::read_to_string(ABILITIES_PATH).unwrap();
|
||||||
|
let abilities = serde_json::from_str::<Vec<Ability>>(&abilities_str).unwrap();
|
||||||
|
|
||||||
|
tracing::info!("Loading finishers json data from {}", ABILITIES_PATH);
|
||||||
|
let finishers_str = std::fs::read_to_string(FINISHERS_PATH).unwrap();
|
||||||
|
let finishers = serde_json::from_str::<HashMap<String, Finisher>>(&finishers_str).unwrap();
|
||||||
|
|
||||||
|
tracing::info!("Loading commands json data from {}", ABILITIES_PATH);
|
||||||
|
let commands_str = std::fs::read_to_string(COMMANDS_PATH).unwrap();
|
||||||
|
let mut commands = serde_json::from_str::<Vec<Command>>(&commands_str).unwrap();
|
||||||
|
|
||||||
|
// Create a vec with all the crystal variants found in abilities
|
||||||
|
let crystals = abilities
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.from.clone())
|
||||||
|
.unique()
|
||||||
|
.sorted()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Create a vec of crystals and what ability they give for each recipe
|
||||||
|
for cmd in commands.iter_mut() {
|
||||||
|
for recipe in cmd.recipes.iter_mut() {
|
||||||
|
recipe.set_abilities(&abilities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Generating the commands table template");
|
||||||
|
let template = CommandsTemplate { commands, crystals };
|
||||||
|
|
||||||
|
std::fs::write("./out/index.html", template.render().unwrap()).unwrap();
|
||||||
|
}
|
161
src/main.rs
161
src/main.rs
|
@ -1,136 +1,9 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use askama::Template;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
mod bbs;
|
||||||
enum Character {
|
|
||||||
#[serde(alias = "A")]
|
|
||||||
Aqua,
|
|
||||||
#[serde(alias = "V")]
|
|
||||||
Ventus,
|
|
||||||
#[serde(alias = "T")]
|
|
||||||
Terra,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct Ability {
|
|
||||||
name: String,
|
|
||||||
from: String,
|
|
||||||
category: String,
|
|
||||||
max: u8,
|
|
||||||
types: Vec<char>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Command {
|
|
||||||
name: String,
|
|
||||||
category: String,
|
|
||||||
char: Vec<char>,
|
|
||||||
#[serde(default)]
|
|
||||||
starting: Option<bool>,
|
|
||||||
#[serde(default)]
|
|
||||||
info: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
recipes: Vec<CommandRecipe>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct CommandRecipe {
|
|
||||||
char: Vec<Character>,
|
|
||||||
r#type: char,
|
|
||||||
ingredients: (String, String),
|
|
||||||
chance: u8,
|
|
||||||
#[serde(skip_deserializing)]
|
|
||||||
abilities: Vec<(String, Ability)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandRecipe {
|
|
||||||
pub fn get_ability(&self, crystal: &str) -> Option<Ability> {
|
|
||||||
if self.r#type == '-' {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ability in self.abilities.iter() {
|
|
||||||
if ability.0 == crystal {
|
|
||||||
return Some(ability.1.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should never happen unless the json files are wrong
|
|
||||||
panic!(
|
|
||||||
"No ability found for {} + {} and {}",
|
|
||||||
self.ingredients.0, self.ingredients.1, crystal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_unlock(&self, char: Character) -> bool {
|
|
||||||
self.char.contains(&char)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_unlock_chars(&self) -> String {
|
|
||||||
let mut id = String::new();
|
|
||||||
if self.can_unlock(Character::Terra) {
|
|
||||||
id += "T"
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.can_unlock(Character::Ventus) {
|
|
||||||
if !id.is_empty() {
|
|
||||||
id += " ";
|
|
||||||
}
|
|
||||||
id += "V";
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.can_unlock(Character::Aqua) {
|
|
||||||
if !id.is_empty() {
|
|
||||||
id += " ";
|
|
||||||
}
|
|
||||||
id += "A";
|
|
||||||
}
|
|
||||||
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_abilities(&mut self, abilities: &[Ability]) {
|
|
||||||
let mut vec: Vec<(String, Ability)> = vec![];
|
|
||||||
|
|
||||||
for ability in abilities.iter() {
|
|
||||||
if ability.types.contains(&self.r#type) {
|
|
||||||
vec.push((ability.from.clone(), ability.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vec.sort();
|
|
||||||
self.abilities = vec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Finisher {
|
|
||||||
name: String,
|
|
||||||
char: Vec<Character>,
|
|
||||||
level: u8,
|
|
||||||
#[serde(default)]
|
|
||||||
follows: Vec<String>,
|
|
||||||
goal: String,
|
|
||||||
color: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "pages/commands.html", whitespace = "suppress")]
|
|
||||||
struct CommandsTemplate {
|
|
||||||
pub commands: Vec<Command>,
|
|
||||||
pub crystals: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
const ABILITIES_PATH: &str = "./input/abilities.json";
|
|
||||||
const FINISHERS_PATH: &str = "./input/finish-commands.json";
|
|
||||||
const COMMANDS_PATH: &str = "./input/commands.json";
|
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -140,35 +13,5 @@ fn main() {
|
||||||
.event_format(tracing_subscriber::fmt::format().with_source_location(true))
|
.event_format(tracing_subscriber::fmt::format().with_source_location(true))
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
tracing::info!("Loading abilities json data from {}", ABILITIES_PATH);
|
bbs::init();
|
||||||
let abilities_str = std::fs::read_to_string(ABILITIES_PATH).unwrap();
|
|
||||||
let abilities = serde_json::from_str::<Vec<Ability>>(&abilities_str).unwrap();
|
|
||||||
|
|
||||||
tracing::info!("Loading finishers json data from {}", ABILITIES_PATH);
|
|
||||||
let finishers_str = std::fs::read_to_string(FINISHERS_PATH).unwrap();
|
|
||||||
let finishers = serde_json::from_str::<HashMap<String, Finisher>>(&finishers_str).unwrap();
|
|
||||||
|
|
||||||
tracing::info!("Loading commands json data from {}", ABILITIES_PATH);
|
|
||||||
let commands_str = std::fs::read_to_string(COMMANDS_PATH).unwrap();
|
|
||||||
let mut commands = serde_json::from_str::<Vec<Command>>(&commands_str).unwrap();
|
|
||||||
|
|
||||||
// Create a vec with all the crystal variants found in abilities
|
|
||||||
let crystals = abilities
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.from.clone())
|
|
||||||
.unique()
|
|
||||||
.sorted()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Create a vec of crystals and what ability they give for each recipe
|
|
||||||
for cmd in commands.iter_mut() {
|
|
||||||
for recipe in cmd.recipes.iter_mut() {
|
|
||||||
recipe.set_abilities(&abilities);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!("Generating the commands table template");
|
|
||||||
let template = CommandsTemplate { commands, crystals };
|
|
||||||
|
|
||||||
std::fs::write("./index.html", template.render().unwrap()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Commands{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script>
|
||||||
|
let charFilter = "";
|
||||||
|
let typeFilter = "";
|
||||||
|
let searchType = "commands";
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
const searchFilter = document.getElementById("filter");
|
||||||
|
let filterHandler = debounce(() => filter());
|
||||||
|
searchFilter.addEventListener("keyup", filterHandler);
|
||||||
|
searchFilter.placeholder = "Search commands...";
|
||||||
|
|
||||||
|
const searchInputs = document.querySelectorAll(
|
||||||
|
'input[type="radio"][name="search"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
searchInputs.forEach(function (item, index) {
|
||||||
|
item.addEventListener("input", function () {
|
||||||
|
searchType = this.checked ? this.value : "";
|
||||||
|
searchFilter.placeholder = "Search " + this.value + "...";
|
||||||
|
filter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const charFilters = document.querySelectorAll(
|
||||||
|
'input[type="radio"][name="charFilter"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
charFilters.forEach(function (item, index) {
|
||||||
|
item.addEventListener("input", function () {
|
||||||
|
charFilter = this.checked ? this.value : "";
|
||||||
|
filter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeFilters = document.querySelectorAll(
|
||||||
|
'input[type="radio"][name="typeFilter"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
typeFilters.forEach(function (item, index) {
|
||||||
|
item.addEventListener("input", function () {
|
||||||
|
typeFilter = this.checked ? this.value : "";
|
||||||
|
filter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function debounce(callback, wait = 300) {
|
||||||
|
let timeoutId = null;
|
||||||
|
return (...args) => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
callback(...args);
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter() {
|
||||||
|
const table = document.querySelector("table tbody");
|
||||||
|
const search = document
|
||||||
|
.getElementById("filter")
|
||||||
|
.value.toLowerCase();
|
||||||
|
|
||||||
|
for (const child of table.children) {
|
||||||
|
const tds = child.children;
|
||||||
|
resetStyle(child, tds);
|
||||||
|
|
||||||
|
if (search.length > 0) {
|
||||||
|
if (searchType === "commands") {
|
||||||
|
// Check for command name
|
||||||
|
if (!tds[1].innerText.toLowerCase().includes(search)) {
|
||||||
|
child.style.display = "none";
|
||||||
|
} else {
|
||||||
|
applyStyle(tds[1]);
|
||||||
|
}
|
||||||
|
} else if (searchType === "ingredients") {
|
||||||
|
// Ingredients
|
||||||
|
if (!tds[2].innerText.toLowerCase().includes(search)) {
|
||||||
|
if (
|
||||||
|
!tds[3].innerText.toLowerCase().includes(search)
|
||||||
|
) {
|
||||||
|
child.style.display = "none";
|
||||||
|
} else {
|
||||||
|
applyStyle(tds[3]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applyStyle(tds[2]);
|
||||||
|
if (
|
||||||
|
tds[3].innerText.toLowerCase().includes(search)
|
||||||
|
) {
|
||||||
|
applyStyle(tds[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (searchType === "abilities") {
|
||||||
|
// Abilities
|
||||||
|
hasLine = false;
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const id = i + 6;
|
||||||
|
const ablName = tds[id].innerText.toLowerCase();
|
||||||
|
if (ablName.includes(search)) {
|
||||||
|
applyStyle(tds[id]);
|
||||||
|
hasLine = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasLine) {
|
||||||
|
child.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeCheck =
|
||||||
|
typeFilter === "" || tds[1].className === typeFilter;
|
||||||
|
const charCheck =
|
||||||
|
charFilter === "" || child.className.includes(charFilter);
|
||||||
|
|
||||||
|
if (child.style.display == "" && (!typeCheck || !charCheck)) {
|
||||||
|
child.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetStyle(child, tds) {
|
||||||
|
child.style.display = "";
|
||||||
|
for (const td of tds) {
|
||||||
|
td.style.fontWeight = "inherit";
|
||||||
|
td.style.color = "#fff";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStyle(el) {
|
||||||
|
el.style.fontWeight = "bold";
|
||||||
|
el.style.color = "red";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "components/bbs/search.html" %}
|
||||||
|
<br />
|
||||||
|
{% include "components/bbs/type-filters.html" %}
|
||||||
|
<br />
|
||||||
|
{% include "components/bbs/char-filters.html" %}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2">Character</th>
|
||||||
|
<th rowspan="2">Command</th>
|
||||||
|
<th rowspan="2">Ingredient A</th>
|
||||||
|
<th rowspan="2">Ingredient B</th>
|
||||||
|
<th rowspan="2">Type</th>
|
||||||
|
<th rowspan="2">Chance</th>
|
||||||
|
<th colspan="7">Abilities</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{% for crystal in crystals %}
|
||||||
|
<th>{{ crystal }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for cmd in commands %}
|
||||||
|
{% for recipe in cmd.recipes %}
|
||||||
|
<tr class="{{ recipe.get_unlock_chars() }}">
|
||||||
|
<td>
|
||||||
|
<div class="charlist">
|
||||||
|
<!-- RGB moment -->
|
||||||
|
<span class="terra">
|
||||||
|
{% if recipe.can_unlock(Character::Terra) %}T{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="ventus">
|
||||||
|
{% if recipe.can_unlock(Character::Ventus) %}V{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="aqua">
|
||||||
|
{% if recipe.can_unlock(Character::Aqua) %}A{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="{{ cmd.category }}">{{ cmd.name }}</td>
|
||||||
|
<td>{{ recipe.ingredients.0 }}</td>
|
||||||
|
<td>{{ recipe.ingredients.1 }}</td>
|
||||||
|
<td>{{ recipe.type }}</td>
|
||||||
|
<td>{{ recipe.chance }}%</td>
|
||||||
|
{% for crystal in crystals %}
|
||||||
|
{% let ability = recipe.get_ability(crystal) %}
|
||||||
|
<td>
|
||||||
|
{% if ability.is_some() %}
|
||||||
|
{{ ability.unwrap().name }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue