second commit

This commit is contained in:
Mechseroms 2026-05-18 15:02:03 -05:00
parent c1ad629e79
commit adf7f20e58
42 changed files with 1859 additions and 802 deletions

Binary file not shown.

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser, Character, Feature, Package, PackageFeature, Pin
from .models import CustomUser, Character, Feature, Package, PackageFeature, Pin, Asset, ObjectTrait
class CustomUserAdmin(UserAdmin):
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'uuid')
@ -40,3 +40,13 @@ class PackageFeatureAdmin(admin.ModelAdmin):
class PinAdmin(admin.ModelAdmin):
list_display = ('label', 'url', 'x', 'y')
search_fields = ('label', 'url')
@admin.register(Asset)
class AssetAdmin(admin.ModelAdmin):
list_display = ('asset_name', 'asset_system')
search_fields = ('asset_name', 'asset_system')
@admin.register(ObjectTrait)
class ObjectTraitAdmin(admin.ModelAdmin):
list_display = ('trait_name',)
search_fields = ('trait_name',)

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0 on 2025-12-14 02:30
# Generated by Django 6.0.5 on 2026-05-10 16:07
import django.contrib.auth.models
import django.contrib.auth.validators
@ -19,22 +19,34 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='Feature',
name='Asset',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('feature_name', models.CharField(max_length=200)),
('feature_description', models.TextField(blank=True)),
('feature_data', models.JSONField(blank=True, default=dict)),
('asset_name', models.CharField(max_length=256)),
('asset_description', models.TextField(blank=True)),
('asset_doc_md', models.TextField(blank=True)),
('asset_system', models.CharField(blank=True, max_length=32)),
('asset_requirements', models.JSONField(blank=True, default=list)),
],
),
migrations.CreateModel(
name='Package',
name='ObjectTrait',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('package_name', models.CharField(max_length=200)),
('package_description', models.TextField(blank=True)),
('package_type', models.CharField(blank=True, max_length=100)),
('package_doc_md', models.TextField(blank=True)),
('trait_name', models.CharField(max_length=64)),
('trait_description', models.TextField(blank=True)),
('trait_system', models.CharField(blank=True, max_length=32)),
],
),
migrations.CreateModel(
name='Pin',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=100)),
('url', models.URLField(max_length=300)),
('x', models.FloatField(help_text='X position as percentage (0-100)')),
('y', models.FloatField(help_text='Y position as percentage (0-100)')),
('pin_type', models.CharField(default='general', max_length=100)),
],
),
migrations.CreateModel(
@ -64,10 +76,36 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Feature',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('feature_name', models.CharField(max_length=200)),
('feature_system', models.CharField(blank=True, max_length=36)),
('feature_description', models.TextField(blank=True)),
('feature_requirements', models.JSONField(blank=True, default=list)),
('feature_data', models.JSONField(blank=True, default=dict)),
('feature_traits', models.ManyToManyField(blank=True, related_name='features', to='main.objecttrait')),
],
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('package_name', models.CharField(max_length=200)),
('package_system', models.CharField(blank=True, max_length=36)),
('package_description', models.TextField(blank=True)),
('package_type', models.CharField(blank=True, max_length=100)),
('package_doc_md', models.TextField(blank=True)),
('package_requirements', models.JSONField(blank=True, default=list)),
('package_operations', models.JSONField(blank=True, default=dict)),
('package_traits', models.ManyToManyField(blank=True, related_name='packages', to='main.objecttrait')),
],
),
migrations.CreateModel(
name='Character',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('level', models.IntegerField(default=0)),
('alignment', models.CharField(blank=True, max_length=50)),
@ -84,37 +122,21 @@ class Migration(migrations.Migration):
('wisdom_base', models.IntegerField(default=0)),
('charisma_base', models.IntegerField(default=0)),
('armor_base', models.IntegerField(default=0)),
('strength', models.IntegerField(default=0)),
('dexterity', models.IntegerField(default=0)),
('constitution', models.IntegerField(default=0)),
('intelligence', models.IntegerField(default=0)),
('wisdom', models.IntegerField(default=0)),
('charisma', models.IntegerField(default=0)),
('armor', models.IntegerField(default=0)),
('strength_modifier', models.IntegerField(default=0)),
('dexterity_modifier', models.IntegerField(default=0)),
('constitution_modifier', models.IntegerField(default=0)),
('intelligence_modifier', models.IntegerField(default=0)),
('wisdom_modifier', models.IntegerField(default=0)),
('charisma_modifier', models.IntegerField(default=0)),
('proficiency', models.IntegerField(default=0)),
('inspiration', models.BooleanField(default=False)),
('experience', models.IntegerField(default=0)),
('proficiencies_armor', models.JSONField(blank=True, default=list)),
('proficiencies_weapons', models.JSONField(blank=True, default=list)),
('proficiencies_tools', models.JSONField(blank=True, default=list)),
('languages', models.JSONField(blank=True, default=list)),
('strength_save', models.IntegerField(default=0)),
('dexterity_save', models.IntegerField(default=0)),
('constitution_save', models.IntegerField(default=0)),
('intelligence_save', models.IntegerField(default=0)),
('wisdom_save', models.IntegerField(default=0)),
('charisma_save', models.IntegerField(default=0)),
('strength_save_prof', models.BooleanField(default=False)),
('dexterity_save_prof', models.BooleanField(default=False)),
('constitution_save_prof', models.BooleanField(default=False)),
('intelligence_save_prof', models.BooleanField(default=False)),
('wisdom_save_prof', models.BooleanField(default=False)),
('charisma_save_prof', models.BooleanField(default=False)),
('hp', models.IntegerField(default=0)),
('hp_max', models.IntegerField(default=0)),
('hp_temp', models.IntegerField(default=0)),
('hit_die', models.CharField(blank=True, max_length=20)),
('initiative', models.IntegerField(default=0)),
('deathsaves_successes', models.IntegerField(default=0)),
('deathsaves_failures', models.IntegerField(default=0)),
('fire_resistance', models.BooleanField(default=False)),
@ -137,45 +159,6 @@ class Migration(migrations.Migration):
('blindsight', models.IntegerField(default=0)),
('tremorsense', models.IntegerField(default=0)),
('truesight', models.IntegerField(default=0)),
('athletics', models.IntegerField(default=0)),
('acrobatics', models.IntegerField(default=0)),
('sleight_of_hand', models.IntegerField(default=0)),
('stealth', models.IntegerField(default=0)),
('arcana', models.IntegerField(default=0)),
('history', models.IntegerField(default=0)),
('investigation', models.IntegerField(default=0)),
('nature', models.IntegerField(default=0)),
('religion', models.IntegerField(default=0)),
('animal_handling', models.IntegerField(default=0)),
('insight', models.IntegerField(default=0)),
('medicine', models.IntegerField(default=0)),
('perception', models.IntegerField(default=0)),
('survival', models.IntegerField(default=0)),
('deception', models.IntegerField(default=0)),
('intimidation', models.IntegerField(default=0)),
('performance', models.IntegerField(default=0)),
('persuasion', models.IntegerField(default=0)),
('athletics_passive', models.IntegerField(default=0)),
('acrobatics_passive', models.IntegerField(default=0)),
('sleight_of_hand_passive', models.IntegerField(default=0)),
('stealth_passive', models.IntegerField(default=0)),
('arcana_passive', models.IntegerField(default=0)),
('history_passive', models.IntegerField(default=0)),
('investigation_passive', models.IntegerField(default=0)),
('nature_passive', models.IntegerField(default=0)),
('religion_passive', models.IntegerField(default=0)),
('animal_handling_passive', models.IntegerField(default=0)),
('insight_passive', models.IntegerField(default=0)),
('medicine_passive', models.IntegerField(default=0)),
('perception_passive', models.IntegerField(default=0)),
('survival_passive', models.IntegerField(default=0)),
('deception_passive', models.IntegerField(default=0)),
('intimidation_passive', models.IntegerField(default=0)),
('performance_passive', models.IntegerField(default=0)),
('persuasion_passive', models.IntegerField(default=0)),
('passive_perception', models.IntegerField(default=0)),
('passive_investigation', models.IntegerField(default=0)),
('passive_insight', models.IntegerField(default=0)),
('spellcasting_ability', models.CharField(blank=True, max_length=50)),
('spell_save_dc', models.IntegerField(default=0)),
('spell_attack_bonus', models.IntegerField(default=0)),
@ -192,16 +175,17 @@ class Migration(migrations.Migration):
('gp', models.IntegerField(default=0)),
('pp', models.IntegerField(default=0)),
('equipment', models.JSONField(blank=True, default=list)),
('features', models.JSONField(blank=True, default=list)),
('traits', models.JSONField(blank=True, default=list)),
('personality_traits', models.JSONField(blank=True, default=list)),
('ideals', models.JSONField(blank=True, default=list)),
('bonds', models.JSONField(blank=True, default=list)),
('flaws', models.JSONField(blank=True, default=list)),
('notes', models.TextField(blank=True)),
('hp_features', models.JSONField(blank=True, default=list)),
('custom_attributes', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='characters', to=settings.AUTH_USER_MODEL)),
('features', models.ManyToManyField(blank=True, related_name='characters', to='main.feature')),
('background', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='background_characters', to='main.package')),
('char_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='class_characters', to='main.package')),
('packages', models.ManyToManyField(blank=True, related_name='characters', to='main.package')),
@ -215,6 +199,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('priority', models.IntegerField(default=0)),
('requirements_override', models.JSONField(blank=True, null=True)),
('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.feature')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.package')),
],

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0 on 2025-12-14 23:19
# Generated by Django 6.0.5 on 2026-05-10 18:11
from django.db import migrations, models
@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0009_character_hp_features'),
('main', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='character',
name='hp_features',
model_name='package',
name='package_operations',
field=models.JSONField(blank=True, default=list),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 16:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Pin',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=100)),
('url', models.URLField(max_length=300)),
('x', models.FloatField(help_text='X position as percentage (0-100)')),
('y', models.FloatField(help_text='Y position as percentage (0-100)')),
],
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 18:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0002_pin'),
]
operations = [
migrations.AddField(
model_name='pin',
name='pin_type',
field=models.CharField(default='general', max_length=100),
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 21:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0003_pin_pin_type'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='features',
),
migrations.AddField(
model_name='character',
name='features',
field=models.ManyToManyField(blank=True, related_name='characters', to='main.feature'),
),
]

View File

@ -1,123 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 21:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0004_remove_character_features_character_features'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='armor',
),
migrations.RemoveField(
model_name='character',
name='charisma',
),
migrations.RemoveField(
model_name='character',
name='charisma_modifier',
),
migrations.RemoveField(
model_name='character',
name='charisma_save',
),
migrations.RemoveField(
model_name='character',
name='constitution',
),
migrations.RemoveField(
model_name='character',
name='constitution_modifier',
),
migrations.RemoveField(
model_name='character',
name='constitution_save',
),
migrations.RemoveField(
model_name='character',
name='dexterity',
),
migrations.RemoveField(
model_name='character',
name='dexterity_modifier',
),
migrations.RemoveField(
model_name='character',
name='dexterity_save',
),
migrations.RemoveField(
model_name='character',
name='intelligence',
),
migrations.RemoveField(
model_name='character',
name='intelligence_modifier',
),
migrations.RemoveField(
model_name='character',
name='intelligence_save',
),
migrations.RemoveField(
model_name='character',
name='proficiency',
),
migrations.RemoveField(
model_name='character',
name='strength',
),
migrations.RemoveField(
model_name='character',
name='strength_modifier',
),
migrations.RemoveField(
model_name='character',
name='strength_save',
),
migrations.RemoveField(
model_name='character',
name='wisdom',
),
migrations.RemoveField(
model_name='character',
name='wisdom_modifier',
),
migrations.RemoveField(
model_name='character',
name='wisdom_save',
),
migrations.AddField(
model_name='character',
name='charisma_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='constitution_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='dexterity_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='intelligence_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='strength_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='wisdom_save_prof',
field=models.BooleanField(default=False),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 21:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0005_remove_character_armor_remove_character_charisma_and_more'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='athletics',
),
]

View File

@ -1,165 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 22:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0006_remove_character_athletics'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='acrobatics',
),
migrations.RemoveField(
model_name='character',
name='acrobatics_passive',
),
migrations.RemoveField(
model_name='character',
name='animal_handling',
),
migrations.RemoveField(
model_name='character',
name='animal_handling_passive',
),
migrations.RemoveField(
model_name='character',
name='arcana',
),
migrations.RemoveField(
model_name='character',
name='arcana_passive',
),
migrations.RemoveField(
model_name='character',
name='athletics_passive',
),
migrations.RemoveField(
model_name='character',
name='deception',
),
migrations.RemoveField(
model_name='character',
name='deception_passive',
),
migrations.RemoveField(
model_name='character',
name='history',
),
migrations.RemoveField(
model_name='character',
name='history_passive',
),
migrations.RemoveField(
model_name='character',
name='insight',
),
migrations.RemoveField(
model_name='character',
name='insight_passive',
),
migrations.RemoveField(
model_name='character',
name='intimidation',
),
migrations.RemoveField(
model_name='character',
name='intimidation_passive',
),
migrations.RemoveField(
model_name='character',
name='investigation',
),
migrations.RemoveField(
model_name='character',
name='investigation_passive',
),
migrations.RemoveField(
model_name='character',
name='medicine',
),
migrations.RemoveField(
model_name='character',
name='medicine_passive',
),
migrations.RemoveField(
model_name='character',
name='nature',
),
migrations.RemoveField(
model_name='character',
name='nature_passive',
),
migrations.RemoveField(
model_name='character',
name='passive_insight',
),
migrations.RemoveField(
model_name='character',
name='passive_investigation',
),
migrations.RemoveField(
model_name='character',
name='passive_perception',
),
migrations.RemoveField(
model_name='character',
name='perception',
),
migrations.RemoveField(
model_name='character',
name='perception_passive',
),
migrations.RemoveField(
model_name='character',
name='performance',
),
migrations.RemoveField(
model_name='character',
name='performance_passive',
),
migrations.RemoveField(
model_name='character',
name='persuasion',
),
migrations.RemoveField(
model_name='character',
name='persuasion_passive',
),
migrations.RemoveField(
model_name='character',
name='religion',
),
migrations.RemoveField(
model_name='character',
name='religion_passive',
),
migrations.RemoveField(
model_name='character',
name='sleight_of_hand',
),
migrations.RemoveField(
model_name='character',
name='sleight_of_hand_passive',
),
migrations.RemoveField(
model_name='character',
name='stealth',
),
migrations.RemoveField(
model_name='character',
name='stealth_passive',
),
migrations.RemoveField(
model_name='character',
name='survival',
),
migrations.RemoveField(
model_name='character',
name='survival_passive',
),
]

View File

@ -1,21 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 23:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0007_remove_character_acrobatics_and_more'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='hp_max',
),
migrations.RemoveField(
model_name='character',
name='initiative',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2025-12-14 23:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0008_remove_character_hp_max_remove_character_initiative'),
]
operations = [
migrations.AddField(
model_name='character',
name='hp_features',
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2025-12-15 00:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0010_alter_character_hp_features'),
]
operations = [
migrations.AddField(
model_name='feature',
name='feature_requirements',
field=models.JSONField(default=dict),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2025-12-15 00:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0011_feature_feature_requirements'),
]
operations = [
migrations.AddField(
model_name='packagefeature',
name='requirements_override',
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2025-12-15 01:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0012_packagefeature_requirements_override'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='feature_requirements',
field=models.JSONField(default=list),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 6.0.5 on 2026-05-06 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0013_alter_feature_feature_requirements'),
]
operations = [
migrations.AlterField(
model_name='character',
name='features',
field=models.ManyToManyField(blank=True, related_name='+', to='main.feature'),
),
migrations.AlterField(
model_name='character',
name='packages',
field=models.ManyToManyField(blank=True, related_name='+', to='main.package'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 6.0.5 on 2026-05-06 22:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0014_alter_character_features_alter_character_packages'),
]
operations = [
migrations.AlterField(
model_name='character',
name='features',
field=models.ManyToManyField(blank=True, related_name='characters', to='main.feature'),
),
migrations.AlterField(
model_name='character',
name='packages',
field=models.ManyToManyField(blank=True, related_name='characters', to='main.package'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 6.0.5 on 2026-05-06 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0015_alter_character_features_alter_character_packages'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='feature_requirements',
field=models.JSONField(blank=True, default=list),
),
]

View File

@ -92,6 +92,8 @@ def apply_operations(base, operations, character):
# Character model based on character_template.json
class Character(models.Model):
objects = models.Manager()
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
owner = models.ForeignKey('CustomUser', on_delete=models.CASCADE, related_name='characters')
name = models.CharField(max_length=100)
level = models.IntegerField(default=0)
@ -576,6 +578,7 @@ class Character(models.Model):
class Feature(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
feature_name = models.CharField(max_length=200)
feature_system = models.CharField(max_length=36, blank=True)
feature_description = models.TextField(blank=True)
feature_requirements = models.JSONField(default=list, blank=True)
#feature requirements requires 3 keys, {"property", "value", "condition"}
@ -583,10 +586,18 @@ class Feature(models.Model):
#feature_data holds a value of {"operations", "sources"}
# ----> operations is a list of dictionaries that have to have 6 values
# ----> {"attr", "value", "operation", "limits", "operation_requirements", "priority"}
feature_traits = models.ManyToManyField('ObjectTrait', blank=True, related_name='features')
def __str__(self):
return self.feature_name
def to_json(self):
return {
'id': str(self.id),
'feature_name': self.feature_name,
'feature_description': self.feature_description
}
# Package <-> Feature through model for priorities
class PackageFeature(models.Model):
package = models.ForeignKey('Package', on_delete=models.CASCADE)
@ -605,15 +616,46 @@ class PackageFeature(models.Model):
class Package(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
package_name = models.CharField(max_length=200)
package_system = models.CharField(max_length=36, blank=True)
package_description = models.TextField(blank=True)
package_type = models.CharField(max_length=100, blank=True)
package_doc_md = models.TextField(blank=True)
package_requirements = models.JSONField(default=list, blank=True)
#feature requirements requires 3 keys, {"property", "value", "condition"}
package_operations = models.JSONField(default=list, blank=True)
#feature_data holds a value of {"operations", "sources"}
# ----> operations is a list of dictionaries that have to have 6 values
# ----> {"attr", "value", "operation", "limits", "operation_requirements", "priority"}
features = models.ManyToManyField('Feature', through='PackageFeature', blank=True, related_name='packages')
package_traits = models.ManyToManyField('ObjectTrait', blank=True, related_name='packages')
def __str__(self):
return self.package_name
# Create your models here.
class Asset(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
asset_name = models.CharField(max_length=256)
asset_description = models.TextField(blank=True)
asset_doc_md = models.TextField(blank=True)
asset_system = models.CharField(max_length=32, blank=True)
asset_requirements = models.JSONField(default=list, blank=True)
class ObjectTrait(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
trait_name = models.CharField(max_length=64)
trait_description = models.TextField(blank=True)
trait_system = models.CharField(max_length=32, blank=True)
def __str__(self):
return self.trait_name
def to_json(self):
return {
'id': str(self.id),
'trait_name': self.trait_name
}
class Pin(models.Model):
label = models.CharField(max_length=100)
url = models.URLField(max_length=300)

View File

@ -39,7 +39,7 @@
</h1>
<hr class="uk-divider-icon uk-margin-small-top uk-margin-bottom">
<div class="uk-grid-small uk-child-width-1-2@s uk-child-width-1-4@m" uk-grid>
<div><label>Race:</label> <span class="uk-badge uk-margin-small-right">{{ character.race }}</span> {{ character.subrace }}</div>
<div><label>Race:</label> {{ character.race }} {{ character.subrace }}</div>
<div><label>Class:</label> {{ character.char_class.package_name }} {{ character.subclass }}</div>
<div><label>Background:</label> {{ character.background }}</div>
<div><label>Alignment:</label> {{ character.alignment }}</div>

View File

@ -7,6 +7,15 @@
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
</head>
<body>
<div id="feature-form-container">
<form id="feature-form"
hx-post="/{{ system }}/feature/new"
hx-trigger="submit"
hx-target="#feature-form-container"
hx-swap="outerHTML"
method="POST"
autocomplete="off">
{% csrf_token %}
<div class="uk-container">
<div uk-grid>
<div class="uk-width-1-1">
@ -80,151 +89,209 @@
</div>
<div class="uk-width-1-1">
<input type="hidden" id="feature_payload" name="feature_payload">
<button type="submit" class="uk-button uk-button-primary uk-width-1-1">Submit Feature</button>
</div>
</div>
</div>
</form>
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script>
// --- DYNAMIC REQUIREMENTS ---
function addRequirement(prop='', cond='', val='') {
const reqList = document.getElementById('requirements-list');
const idx = reqList.children.length;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
el.setAttribute('uk-grid', '')
el.innerHTML = `
<input class="uk-input" name="requirement_property_${idx}" type="text" placeholder="Property" value="${prop}">
<select class="uk-select" name="requirement_condition_${idx}">
<option value="==" ${cond=='==' ? 'selected' : ''}>&equals;</option>
<option value=">=" ${cond=='>=' ? 'selected' : ''}>&ge;</option>
<option value="<=" ${cond=='<=' ? 'selected' : ''}>&le;</option>
<option value="!=" ${cond=='!=' ? 'selected' : ''}>&ne;</option>
</select>
<input class="uk-input" name="requirement_value_${idx}" type="text" placeholder="Value" value="${val}">
<button type="button" class="uk-button uk-button-danger uk-button-small" onclick="this.parentElement.remove()"><span uk-icon="icon: minus"></span></button>
`;
reqList.appendChild(el);
}
// --- DYNAMIC OPERATIONS ---
function addOperation(attr='', op='', value='', extra=[]) {
const operationsBody = document.getElementById('operations-body');
const op_index = operationsBody.children.length;
const main_div = document.createElement('div');
main_div.setAttribute('class', 'uk-width-1-1')
//const el = document.createElement('tr');
main_div.innerHTML = `
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-4">
<input class="uk-input" name="operation_attr_${op_index}" type="text" placeholder="Affected Attribute (e.g., strength)" value="${attr}">
</div>
<div class="uk-width-1-4">
<select class="uk-select" name="operation_operation_${op_index}">
<option value="add" ${op==='add'?'selected':''}>Add</option>
<option value="subtract" ${op==='subtract'?'selected':''}>Subtract</option>
<option value="multiply" ${op==='multiply'?'selected':''}>Multiply</option>
<option value="divide" ${op==='divide'?'selected':''}>Divide</option>
<option value="set" ${op==='set'?'selected':''}>Set</option>
</select>
</div>
<div class="uk-width-1-4">
<input class="uk-input" name="operation_value_${op_index}" type="text" placeholder="Value or Attribute..." value="${value}">
</div>
<div class="uk-width-1-4">
<button type="button" class="uk-button uk-button-danger" onclick="this.closest(".uk-card").remove()"><span uk-icon="icon: trash"></span></button>
</div>
</div>
<div uk-grid>
<div class="uk-width-1-1">
<ul class="uk-accordion-default" uk-accordion>
<li>
<a class="uk-accordion-title" href>Operation Limits</a>
<div class="uk-accordion-content" uk-grid>
<div class="uk-width-1-1">
<p class="uk-text-meta">Add a limit to when this operation is applied to a character.</p>
</div>
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-secondary" onclick="addLimitField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Limit</button>
</div>
<div class="uk-width-1-1 uk-margin-small-top" id="operation-limit-list-${op_index}"></div>
</div>
</li>
<li>
<a class="uk-accordion-title" href>Operation Requirements</a>
<div class="uk-accordion-content">
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-small uk-button-secondary" onclick="addOperationRequirementField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Requirement</button>
</div>
<div class="uk-width-1-1 uk-margin-small-top" id="operation-req-list-${op_index}"></div>
</div>
</li>
</ul>
</div>
</div>
</div>`
operationsBody.appendChild(main_div);
// add pre-existing subparts if any (extra)
if (extra && Array.isArray(extra)) {
extra.forEach(kv => addLimitField(op_index, kv.key, kv.value));
}
}
function addLimitField(opIdx, k='', v='', t='') {
const list = document.getElementById('operation-limit-list-' + opIdx);
const subIdx = (list) ? list.children.length : 0;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle';
el.setAttribute('uk-grid', '');
el.innerHTML = `
<label class="uk-label">Limit</label>
<input class="uk-input" name="operation_${opIdx}_limitkey_${subIdx}" type="text" placeholder="Extra Key (e.g., limit, min, max)" value="${k||''}">
<select class="uk-select" name="operation_${opIdx}_limittype_${subIdx}">
<option value="max" ${t==='max'?'selected':''}>max</option>
<option value="min" ${t==='min'?'selected':''}>min</option>
<option value="equals" ${t==='equals'?'selected':''}>equal</option>
</select>
<input class="uk-input" name="operation_${opIdx}_limitval_${subIdx}" type="text" placeholder="Value" value="${v||''}">
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
`;
list.appendChild(el);
}
// --- DYNAMIC OPERATION REQUIREMENTS ---
function addOperationRequirementField(opIdx, prop='', cond='', val='') {
const list = document.getElementById('operation-req-list-' + opIdx);
const subIdx = (list) ? list.children.length : 0;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
el.setAttribute('uk-grid', '');
el.innerHTML = `
<input class="uk-input" name="operation_${opIdx}_req_property_${subIdx}" type="text" placeholder="Requirement Property" value="${prop}">
<select class="uk-select" name="operation_${opIdx}_req_condition_${subIdx}">
<option value="==" ${cond=='==' ? 'selected' : ''}>&equals;</option>
<option value=">=" ${cond=='>=' ? 'selected' : ''}>&ge;</option>
<option value="<=" ${cond=='<=' ? 'selected' : ''}>&le;</option>
<option value="!=" ${cond=='!=' ? 'selected' : ''}>&ne;</option>
</select>
<input class="uk-input" name="operation_${opIdx}_req_value_${subIdx}" type="text" placeholder="Value" value="${val}">
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
`;
list.appendChild(el);
}
// Prevent accidental form submit via Enter in addable fields
// (recommended for dynamic forms)
document.addEventListener('keydown', function(e) {
if(e.key==="Enter" && e.target.tagName==="INPUT") {
// do nothing if inside textarea
if (!(e.target.closest('form') && e.target.closest('form').id === 'feature-form')) return;
e.preventDefault(); return false;
}
document.getElementById('feature-form').addEventListener('submit', function (e) {
const reqList = document.querySelectorAll('#requirements-list > div');
let requirements = [];
reqList.forEach(div => {
const prop = div.querySelector('[name^="requirement_property_"]')?.value || "";
const condition = div.querySelector('[name^="requirement_condition_"]')?.value || "";
const value = div.querySelector('[name^="requirement_value_"]')?.value || "";
if(prop || condition || value) requirements.push({property: prop, condition, value});
});
const operationsDivs = document.querySelectorAll('#operations-body > div.uk-width-1-1');
let operations = [];
operationsDivs.forEach(div => {
const attr = div.querySelector('[name^="operation_attr_"]')?.value || "";
const operation = div.querySelector('[name^="operation_operation_"]')?.value || "";
const op_val = div.querySelector('[name^="operation_value_"]')?.value || "";
const opIdxMatch = attr.match(/operation_attr_(\d+)/);
let opIdx = null;
if (opIdxMatch) { opIdx = opIdxMatch[1]; }
let limits = [];
if(opIdx !== null) {
const limitList = div.querySelectorAll(`#operation-limit-list-${opIdx}>div`);
limitList.forEach(ldiv => {
const key = ldiv.querySelector(`[name=operation_${opIdx}_limitkey_${ldiv.dataset.idx}]`)?.value || "";
const type = ldiv.querySelector(`[name=operation_${opIdx}_limittype_${ldiv.dataset.idx}]`)?.value || "";
const value = ldiv.querySelector(`[name=operation_${opIdx}_limitval_${ldiv.dataset.idx}]`)?.value || "";
if(key || type || value) limits.push({key, type, value});
});
}
let opRequirements = [];
if(opIdx !== null) {
const reqList = div.querySelectorAll(`#operation-req-list-${opIdx}>div`);
reqList.forEach(rdiv => {
const prop = rdiv.querySelector(`[name=operation_${opIdx}_req_property_${rdiv.dataset.idx}]`)?.value || "";
const condition = rdiv.querySelector(`[name=operation_${opIdx}_req_condition_${rdiv.dataset.idx}]`)?.value || "";
const value = rdiv.querySelector(`[name=operation_${opIdx}_req_value_${rdiv.dataset.idx}]`)?.value || "";
if(prop || condition || value) opRequirements.push({property: prop, condition, value});
});
}
operations.push({attribute: attr, operation, value: op_val, limits, requirements: opRequirements});
});
const feature_data = {
operations: operations
};
const payload = {
feature_name:document.getElementById('feature_name').value,
feature_description: document.getElementById('feature_description').value,
feature_requirements: requirements,
feature_data: feature_data
};
document.getElementById('feature_payload').value = JSON.stringify(payload);
});
function addRequirement(prop='', cond='', val='') {
const reqList = document.getElementById('requirements-list');
const idx = reqList.children.length;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
el.setAttribute('uk-grid', '')
el.innerHTML = `
<input class="uk-input" name="requirement_property_${idx}" type="text" placeholder="Property" value="${prop}">
<select class="uk-select" name="requirement_condition_${idx}">
<option value="==" ${cond=='==' ? 'selected' : ''}>&equals;</option>
<option value=">=" ${cond=='>=' ? 'selected' : ''}>&ge;</option>
<option value="<=" ${cond=='<=' ? 'selected' : ''}>&le;</option>
<option value="!=" ${cond=='!=' ? 'selected' : ''}>&ne;</option>
</select>
<input class="uk-input" name="requirement_value_${idx}" type="text" placeholder="Value" value="${val}">
<button type="button" class="uk-button uk-button-danger uk-button-small" onclick="this.parentElement.remove()"><span uk-icon="icon: minus"></span></button>
`;
reqList.appendChild(el);
}
function addOperation(attr='', op='', value='', extra=[]) {
const operationsBody = document.getElementById('operations-body');
const op_index = operationsBody.children.length;
const main_div = document.createElement('div');
main_div.setAttribute('class', 'uk-width-1-1')
main_div.innerHTML = `
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-4">
<input class="uk-input" name="operation_attr_${op_index}" type="text" placeholder="Affected Attribute (e.g., strength)" value="${attr}">
</div>
<div class="uk-width-1-4">
<select class="uk-select" name="operation_operation_${op_index}">
<option value="add" ${op==='add'?'selected':''}>Add</option>
<option value="subtract" ${op==='subtract'?'selected':''}>Subtract</option>
<option value="multiply" ${op==='multiply'?'selected':''}>Multiply</option>
<option value="divide" ${op==='divide'?'selected':''}>Divide</option>
<option value="set" ${op==='set'?'selected':''}>Set</option>
</select>
</div>
<div class="uk-width-1-4">
<input class="uk-input" name="operation_value_${op_index}" type="text" placeholder="Value or Attribute..." value="${value}">
</div>
<div class="uk-width-1-4">
<button type="button" class="uk-button uk-button-danger" onclick="this.closest(".uk-card").remove()"><span uk-icon="icon: trash"></span></button>
</div>
</div>
<div uk-grid>
<div class="uk-width-1-1">
<ul class="uk-accordion-default" uk-accordion>
<li>
<a class="uk-accordion-title" href>Operation Limits</a>
<div class="uk-accordion-content" uk-grid>
<div class="uk-width-1-1">
<p class="uk-text-meta">Add a limit to when this operation is applied to a character.</p>
</div>
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-secondary" onclick="addLimitField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Limit</button>
</div>
<div class="uk-width-1-1 uk-margin-small-top" id="operation-limit-list-${op_index}"></div>
</div>
</li>
<li>
<a class="uk-accordion-title" href>Operation Requirements</a>
<div class="uk-accordion-content">
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-small uk-button-secondary" onclick="addOperationRequirementField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Requirement</button>
</div>
<div class="uk-width-1-1 uk-margin-small-top" id="operation-req-list-${op_index}"></div>
</div>
</li>
</ul>
</div>
</div>
</div>`
operationsBody.appendChild(main_div);
// add pre-existing subparts if any (extra)
if (extra && Array.isArray(extra)) {
extra.forEach(kv => addLimitField(op_index, kv.key, kv.value));
}
}
function addLimitField(opIdx, k='', v='', t='') {
const list = document.getElementById('operation-limit-list-' + opIdx);
const subIdx = (list) ? list.children.length : 0;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle';
el.setAttribute('uk-grid', '');
el.innerHTML = `
<label class="uk-label">Limit</label>
<input class="uk-input" name="operation_${opIdx}_limitkey_${subIdx}" type="text" placeholder="Extra Key (e.g., limit, min, max)" value="${k||''}">
<select class="uk-select" name="operation_${opIdx}_limittype_${subIdx}">
<option value="max" ${t==='max'?'selected':''}>max</option>
<option value="min" ${t==='min'?'selected':''}>min</option>
<option value="equals" ${t==='equals'?'selected':''}>equal</option>
</select>
<input class="uk-input" name="operation_${opIdx}_limitval_${subIdx}" type="text" placeholder="Value" value="${v||''}">
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
`;
list.appendChild(el);
}
function addOperationRequirementField(opIdx, prop='', cond='', val='') {
const list = document.getElementById('operation-req-list-' + opIdx);
const subIdx = (list) ? list.children.length : 0;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
el.setAttribute('uk-grid', '');
el.innerHTML = `
<input class="uk-input" name="operation_${opIdx}_req_property_${subIdx}" type="text" placeholder="Requirement Property" value="${prop}">
<select class="uk-select" name="operation_${opIdx}_req_condition_${subIdx}">
<option value="==" ${cond=='==' ? 'selected' : ''}>&equals;</option>
<option value=">=" ${cond=='>=' ? 'selected' : ''}>&ge;</option>
<option value="<=" ${cond=='<=' ? 'selected' : ''}>&le;</option>
<option value="!=" ${cond=='!=' ? 'selected' : ''}>&ne;</option>
</select>
<input class="uk-input" name="operation_${opIdx}_req_value_${subIdx}" type="text" placeholder="Value" value="${val}">
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
`;
list.appendChild(el);
}
document.addEventListener('keydown', function(e) {
if(e.key==="Enter" && e.target.tagName==="INPUT") {
if (!(e.target.closest('form') && e.target.closest('form').id === 'feature-form')) return;
e.preventDefault(); return false;
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,78 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Feature Detail</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
</head>
<body>
<div class="uk-container uk-padding">
<h1 class="uk-text-center uk-margin">Feature Detail/Edit<br><span class="subtitle">{{ feature.feature_name }}</span></h1>
<form id="feature-detail-form"
hx-post="/{{ system }}/feature/{{ feature.id }}"
hx-trigger="submit"
hx-target="#feature-detail-form"
hx-swap="outerHTML"
method="POST" autocomplete="off">
{% csrf_token %}
<div class="uk-margin">
<label class="uk-form-label" for="feature_name">Name</label>
<input class="uk-input" id="feature_name" name="feature_name" type="text" value="{{ feature.feature_name }}">
</div>
<div class="uk-margin">
<label class="uk-form-label" for="feature_description">Description</label>
<textarea class="uk-textarea" id="feature_description" name="feature_description" rows="3">{{ feature.feature_description }}</textarea>
</div>
<div class="uk-margin">
<label class="uk-form-label">Feature Requirements</label>
<table class="uk-table reqtable" id="reqtable">
<thead>
<tr>
<th>Attribute</th><th>Condition</th><th>Value</th>
</tr>
</thead>
<tbody>
{% for req in feature.feature_requirements %}
<tr>
<td><input class="uk-input" type="text" value="{{ req.attribute|default:req.property }}"></td>
<td>
<select class="uk-select">
<option value="==" {% if req.condition == "==" %}selected{% endif %}>=</option>
<option value=">=" {% if req.condition == ">=" %}selected{% endif %}>&ge;</option>
<option value="<=" {% if req.condition == "<=" %}selected{% endif %}>&le;</option>
<option value="!=" {% if req.condition == "!=" %}selected{% endif %}>&ne;</option>
</select>
</td>
<td><input class="uk-input" type="text" value="{{ req.value }}"></td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="button" class="uk-button uk-button-default uk-button-xsmall add-row-btn" onclick="addFeatureRequirement()">Add Requirement</button>
</div>
<div>
<h3>Operations
<button id="add-operation-btn" type="button" class="uk-button uk-button-primary uk-button-small uk-margin-small-left">
<span uk-icon="icon: plus"></span> Add Operation
</button>
</h3>
<div id="operations-list"></div>
</div>
<div class="uk-margin">
<button class="uk-button uk-button-primary uk-width-1-1">Save</button>
</div>
</form>
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script>
// Prepopulate window.operations from feature.feature_data in the template context
window.operations = {{ feature.feature_data.operations|safe }};
// -- Copy the relevant JS from your test_feature.html here --
// Ensure you also prepopulate the requirements table as above
// Now your UI can work in edit/detail mode with all functionality
</script>
</body>
</html>

View File

@ -0,0 +1,49 @@
{% load static %}
<table class="uk-table uk-table-divider uk-table-hover">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% if page_obj and page_obj.object_list %}
{% for feature in page_obj.object_list %}
<tr>
<td>{{ feature.feature_name }}</td>
<td>{{ feature.feature_description|truncatewords:17 }}</td>
<td>
<button type="button" class="uk-button uk-button-primary uk-button-small" onclick="window.selectFeature('{{ feature.id }}','{{ feature.feature_name|escapejs }}')">
Select
</button>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3" class="uk-text-muted uk-text-center">No features found.</td>
</tr>
{% endif %}
</tbody>
</table>
{% if page_obj %}
<ul class="uk-pagination uk-flex-center" hx-include="[name=q]">
{% if page_obj.has_previous %}
<li><a href="#" hx-get="?q={{ request.GET.q|urlencode }}&page=1" name="modal-feature-search-page">&laquo; First</a></li>
<li><a href="#" hx-get="?q={{ request.GET.q|urlencode }}&page={{ page_obj.previous_page_number }}">Previous</a></li>
{% else %}
<li class="uk-disabled"><span>&laquo; First</span></li>
<li class="uk-disabled"><span>Previous</span></li>
{% endif %}
<li class="uk-active"><span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li>
{% if page_obj.has_next %}
<li><a hx-get="?q={{ request.GET.q|urlencode }}&page={{ page_obj.next_page_number }}">Next</a></li>
<li><a hx-get="?q={{ request.GET.q|urlencode }}&page={{ page_obj.paginator.num_pages }}">Last &raquo;</a></li>
{% else %}
<li class="uk-disabled"><span>Next</span></li>
<li class="uk-disabled"><span>Last &raquo;</span></li>
{% endif %}
</ul>
{% endif %}

View File

@ -0,0 +1,47 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Features List</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
</head>
<body>
<div class="uk-container uk-margin-top">
<h1 class="uk-heading-line uk-text-center"><span>Features for {{ system|default:'?' }}</span></h1>
{% if features %}
<table class="uk-table uk-table-divider uk-table-hover">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Requirements</th>
<th>View</th>
</tr>
</thead>
<tbody>
{% for feature in features %}
<tr>
<td>{{ feature.feature_name }}</td>
<td>{{ feature.feature_description|truncatewords:15 }}</td>
<td>{% for req in feature.feature_requirements %}
<div><code>{{ req.attr }} {{ req.condition }} {{ req.value }}</code></div>
{% endfor %}
</td>
<td><a class="uk-button uk-button-primary uk-button-small" href="/{{ system }}/feature/{{ feature.id }}">
View
</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="uk-alert-warning uk-padding">No features found for this system.</div>
{% endif %}
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
</body>
</html>

View File

@ -0,0 +1,49 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Packages List</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
</head>
<body>
<div class="uk-container uk-margin-top">
<h1 class="uk-heading-line uk-text-center"><span>Packages for {{ system|default:'?' }}</span></h1>
{% if packages %}
<table class="uk-table uk-table-divider uk-table-hover">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Requirements</th>
<th>View</th>
</tr>
</thead>
<tbody>
{% for package in packages %}
<tr>
<td>{{ package.package_name }}</td>
<td>{{ package.package_type }}</td>
<td>{{ package.package_description|truncatewords:15 }}</td>
<td>{% for req in package.package_requirements %}
<div><code>{{ req.attr }} {{ req.condition }} {{ req.value }}</code></div>
{% endfor %}
</td>
<td><a class="uk-button uk-button-primary uk-button-small" href="/{{ system }}/package/{{ package.id }}">
View
</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="uk-alert-warning uk-padding">No packages found for this system.</div>
{% endif %}
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
</body>
</html>

View File

@ -0,0 +1,475 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Test Feature Builder</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
<style>
.operation-card {
border: 1px solid #dadada;
border-radius: 8px;
margin: 8px 0;
padding: 12px;
background: #fafbfc;
position: relative;
}
.subtitle {
font-size: 14px;
color: #777;
}
table.reqtable { width:100%; font-size:14px; margin-bottom:8px; }
table.reqtable th, table.reqtable td { padding:4px 6px; }
table.reqtable input, table.reqtable select { min-width: 70px; }
.remove-row { color: #d44; font-weight:bold; cursor:pointer; }
.add-row-btn { font-size: 13px; margin-top: 2px; }
summary { font-weight: bold; cursor:pointer; }
</style>
</head>
<body>
<div class="uk-container uk-padding">
<h1 class="uk-text-center uk-margin">Test Feature Builder<br><span class="subtitle">Structured Table Entry UI</span></h1>
<form id="feature-test-form"
hx-post="{% if feature %}/{{ system }}/feature/{{ feature.id }}{% else %}/{{ system }}/feature/new{% endif %}"
hx-trigger="submit"
hx-target="#toast-dock"
hx-swap="innerHTML"
method="POST" autocomplete="off">
{% csrf_token %}
<div class="uk-margin">
<label class="uk-form-label" for="feature_name">Name</label>
<input class="uk-input" id="feature_name" name="feature_name" type="text" value="{{feature.feature_name|default:''}}" required>
</div>
<div class="uk-margin">
<div id="trait-dock"></div>
<input id="trait-ajax-autocomplete" class="uk-input uk-input-small">
<div id="trait-suggestions"></div>
</div>
<div class="uk-width-1-1">
<label class="uk-form-label" for="feature_description">Description</label>
<div class="uk-form-controls">
<textarea class="uk-textarea" id="feature_description" name="feature_description" rows="3">{{feature.feature_description|default:''}}</textarea>
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label">Feature Requirements</label>
<table id="reqtable" class="uk-table reqtable">
<thead>
<tr>
<th>Attribute</th>
<th>Condition</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for req in feature.feature_requirements %}
<tr>
<td><input class="uk-input" type="text" value="{{ req.attr }}"></td>
<td>
<select class="uk-select">
<option value="==" {% if req.condition == "==" %}selected{% endif %}>=</option>
<option value=">=" {% if req.condition == ">=" %}selected{% endif %}>&ge;</option>
<option value="<=" {% if req.condition == "<=" %}selected{% endif %}>&le;</option>
<option value="!=" {% if req.condition == "!=" %}selected{% endif %}>&ne;</option>
</select>
</td>
<td><input class="uk-input" type="text" value="{{ req.value }}"></td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="button" class="uk-button uk-button-default uk-button-xsmall add-row-btn" id="add-feature-req-btn" onclick="addFeatureRequirement()">Add Requirement</button>
</div>
<div>
<h3>Operations
<button id="add-operation-btn" type="button" class="uk-button uk-button-primary uk-button-small uk-margin-small-left">
<span uk-icon="icon: plus"></span> Add Operation
</button>
</h3>
<div id="operations-list"></div>
</div>
<div class="uk-margin">
<button class="uk-button uk-button-primary uk-width-1-1">Submit Feature</button>
</div>
</form>
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script>
var operations = {{ feature.feature_data.operations|default:'[]'|safe }};
var selectedTraits = {{ traits|default:'[]'|safe }};
renderOperations()
renderTraitChips()
console.log(operations)
console.log(selectedTraits)
document.getElementById('trait-ajax-autocomplete').addEventListener('input', function(){
let query = this.value.trim();
if(query.length < 2) return;
fetch(`/traits/search/?system={{ system }}&q=${encodeURIComponent(query)}`)
.then(resp => resp.json())
.then(data => {
let suggestionElement = document.getElementById('trait-suggestions')
if(data.length > 0){
suggestionElement.innerHTML = '<strong>Here are some suggestions: </strong>'
} else {
suggestionElement.innerHTML = ''
}
data.forEach(trait => {
let a_link = document.createElement('a')
a_link.href = "#";
a_link.innerHTML = trait.trait_name
a_link.onclick = function(e){
e.preventDefault();
addTrait(trait);
}
suggestionElement.appendChild(a_link)
suggestionElement.appendChild(document.createTextNode(" "));
});
suggestionElement.style.display = 'block';
});
});
function renderTraitChips(){
let traitDock = document.getElementById('trait-dock');
traitDock.innerHTML = "";
selectedTraits.forEach(tr => {
let traitChip = document.createElement('span');
traitChip.className = "uk-label uk-label-primary uk-margin-small-right uk-margin-small-bottom";
traitChip.innerHTML = `${tr.trait_name}
<a href='#' style='color:white;margin-left:6px' onclick='removeTrait("${tr.id}");return false;'>&times;</a>`;
traitDock.appendChild(traitChip);
});
}
function addTrait(trait){
if (selectedTraits.some(t => t.id === trait.id)) return;
selectedTraits.push(trait)
console.log(selectedTraits)
renderTraitChips()
}
function removeTrait(id){
selectedTraits = selectedTraits.filter(t => String(t.id) !== String(id));
console.log(selectedTraits)
renderTraitChips()
}
function addFeatureRequirement(){
var table = document.getElementById(`reqtable`);
var tbody = table.querySelector('tbody');
var tblRow = document.createElement('tr');
var attrCell = document.createElement('td');
var attrInput = document.createElement('input');
attrInput.type = 'text';
attrInput.className = 'uk-input';
attrCell.appendChild(attrInput);
tblRow.appendChild(attrCell);
var conditionCell = document.createElement('td');
var conditionSelect = document.createElement('select');
conditionSelect.className = 'uk-select';
['==','>=','<=','!='].forEach(function(opt) {
var option = document.createElement('option');
option.value = opt;
option.text = (opt === '==') ? '=' : opt;
conditionSelect.appendChild(option);
});
conditionCell.appendChild(conditionSelect);
tblRow.appendChild(conditionCell);
var valueCell = document.createElement('td');
var valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'uk-input';
valueCell.appendChild(valueInput);
tblRow.appendChild(valueCell);
tbody.appendChild(tblRow)
}
function makeOperationCard(operation, idx) {
const div = document.createElement('div');
div.className = 'operation-card uk-grid-small';
div.innerHTML += `<details ${operation._expanded ? 'open' : ''}>
<summary>Operation #${idx+1}: ${operation.attr || '—'}<span class='subtitle'> (${operation.operation||''})</span></summary>
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-3">
<label>Attribute
<input id="operation-attribute${idx}" class="uk-input op-attribute" placeholder="Attribute" type="text" value="${operation.attr||''}">
</label>
</div>
<div class="uk-width-1-3">
<label>Operation
<select id="operation-operation${idx}" class="uk-select op-operation">
<option value="add" ${operation.operation==='add'?'selected':''}>Add</option>
<option value="subtract" ${operation.operation==='subtract'?'selected':''}>Subtract</option>
<option value="multiply" ${operation.operation==='multiply'?'selected':''}>Multiply</option>
<option value="divide" ${operation.operation==='divide'?'selected':''}>Divide</option>
<option value="set" ${operation.operation==='set'?'selected':''}>Set</option>
</select>
</label>
</div>
<div class="uk-width-1-3">
<label>Value
<input id="operation-value${idx}" class="uk-input op-value" type="text" value="${operation.value||''}">
</label>
</div>
</div>
<hr></hr>
<div class="uk-margin-small">
<div style="margin-top:6px;">
<table class="uk-table" id="limits-table-op${idx}">
<caption>Operations Limits</caption>
<thead>
<tr>
<th>Attribute</th>
<th>Condition</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" class="uk-button uk-button-default uk-width-1-2" onclick=addOperationLimits(${idx})>Add Limit</button>
</div>
<hr></hr>
<div style="margin-top:6px;">
<table class="uk-table uk-table-small" id="reqs-table-op${idx}">
<caption>Operations Requirements</caption>
<thead>
<tr>
<th>Attribute</th>
<th>Condition</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" class="uk-button uk-button-default uk-width-1-2" onclick=addOperationRequirement(${idx})>Add Requirement</button>
</div>
</div>
<button type="button" class="uk-button uk-button-danger uk-button-xsmall remove-op-btn"><span uk-icon="icon: close"></span> Remove Operation</button>
</details>`;
return div;
}
function addOperationLimits(idx){
var table = document.getElementById(`limits-table-op${idx}`);
var tbody = table.querySelector('tbody');
var tblRow = document.createElement('tr');
var attrCell = document.createElement('td');
var attrInput = document.createElement('input');
attrInput.type = 'text';
attrInput.className = 'uk-input';
attrCell.appendChild(attrInput);
tblRow.appendChild(attrCell);
var conditionCell = document.createElement('td');
var conditionSelect = document.createElement('select');
conditionSelect.className = 'uk-select';
['==','>=','<=','!='].forEach(function(opt) {
var option = document.createElement('option');
option.value = opt;
option.text = (opt === '==') ? '=' : opt;
conditionSelect.appendChild(option);
});
conditionCell.appendChild(conditionSelect);
tblRow.appendChild(conditionCell);
var valueCell = document.createElement('td');
var valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'uk-input';
valueCell.appendChild(valueInput);
tblRow.appendChild(valueCell);
tbody.appendChild(tblRow)
}
function addOperationRequirement(idx){
var table = document.getElementById(`reqs-table-op${idx}`);
var tbody = table.querySelector('tbody');
var tblRow = document.createElement('tr');
var attrCell = document.createElement('td');
var attrInput = document.createElement('input');
attrInput.type = 'text';
attrInput.className = 'uk-input';
attrCell.appendChild(attrInput);
tblRow.appendChild(attrCell);
var conditionCell = document.createElement('td');
var conditionSelect = document.createElement('select');
conditionSelect.className = 'uk-select';
['==','>=','<=','!='].forEach(function(opt) {
var option = document.createElement('option');
option.value = opt;
option.text = (opt === '==') ? '=' : opt;
conditionSelect.appendChild(option);
});
conditionCell.appendChild(conditionSelect);
tblRow.appendChild(conditionCell);
var valueCell = document.createElement('td');
var valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'uk-input';
valueCell.appendChild(valueInput);
tblRow.appendChild(valueCell);
tbody.appendChild(tblRow)
}
function readRequestTableRows(tableId){
const table = document.getElementById(tableId);
if(!table) return [];
const rows = table.querySelectorAll('tbody tr');
let arr = [];
console.log(rows)
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if(cells.length>=3){
const attr = cells[0].querySelector('input')?.value || "";
const condition = cells[1].querySelector('select')?.value || "";
const value = cells[2].querySelector('input')?.value || "";
if(attr || condition || value){
arr.push({attr, condition, value});
}
}
})
return arr;
}
function renderOperations() {
const cont = document.getElementById('operations-list');
cont.innerHTML = '';
operations.forEach((op, i) => {
cont.appendChild(makeOperationCard(op, i));
});
}
document.getElementById('add-operation-btn').onclick = function() {
operations.push({ attribute: '', operation: '', value: '', limits: [], requirements: [], _expanded:true });
renderOperations();
};
document.getElementById('feature-test-form').onsubmit = function(e) {
operations.forEach((op, idx) => {
op.attribute = document.getElementById('operation-attribute' + idx).value;
op.operation = document.getElementById('operation-operation' + idx).value;
op.value = document.getElementById('operation-value' + idx).value;
op.limits = readRequestTableRows('limits-table-op' + idx);
op.requirements = readRequestTableRows('reqs-table-op' + idx)
})
let feature_requirements = readRequestTableRows('reqtable')
let payload = {
feature_name: document.getElementById('feature_name').value,
feature_description: document.getElementById('feature_description').value,
feature_requirements: feature_requirements,
feature_data: {
operations: operations.map(({_expanded, ...op})=>op)
},
feature_traits: selectedTraits
};
let field = document.getElementById('feature_payload');
if (!field) {
field = document.createElement('input');
field.type = 'hidden';
field.id = 'feature_payload';
field.name = 'feature_payload';
this.appendChild(field);
}
field.value = JSON.stringify(payload);
console.log(field)
};
</script>
<button class="uk-button uk-button-default uk-button-small uk-float-right" type="button" uk-toggle="target: #sidebar-character-attributes" style="position:fixed;top:16px;right:24px;z-index:1002">
Show D&D Character Attributes
</button>
<div id="sidebar-character-attributes" uk-offcanvas="flip: true; overlay: true">
<div class="uk-offcanvas-bar">
<button class="uk-offcanvas-close" type="button" uk-close></button>
<h3>D&D 5e Character Attributes</h3>
<p>Use these attribute names in your features and operations:</p>
<ul class="uk-list" style="column-count: 1;">
<li>strength</li>
<li>dexterity</li>
<li>constitution</li>
<li>intelligence</li>
<li>wisdom</li>
<li>charisma</li>
<li>level</li>
<li>hp</li>
<li>hp_temp</li>
<li>armor_base</li>
<li>inspiration</li>
<li>proficiency</li>
<li>athletics</li>
<li>acrobatics</li>
<li>sleight_of_hand</li>
<li>stealth</li>
<li>arcana</li>
<li>history</li>
<li>investigation</li>
<li>nature</li>
<li>religion</li>
<li>animal_handling</li>
<li>insight</li>
<li>medicine</li>
<li>perception</li>
<li>survival</li>
<li>deception</li>
<li>intimidation</li>
<li>performance</li>
<li>persuasion</li>
<li>speed_base</li>
<li>darkvision</li>
<li>blindsight</li>
<li>tremorsense</li>
<li>truesight</li>
<li>spell_save_dc</li>
<li>spell_attack_bonus</li>
<li>known_spells</li>
<li>prepared_spells</li>
<li>spell_slots</li>
<li>cantrips</li>
<li>spellcasting_ability</li>
<li>spellcasting_class</li>
</ul>
<p>If you are unsure, check main/models.py <br><small>(scroll sidebar to view more)</small></p>
</div>
</div>
<div id="toast-dock"></div>
</body>
</html>

View File

@ -0,0 +1,621 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Test Package Builder</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
<style>
.operation-card {
border: 1px solid #dadada;
border-radius: 8px;
margin: 8px 0;
padding: 12px;
background: #fafbfc;
position: relative;
}
.subtitle {
font-size: 14px;
color: #777;
}
table.reqtable { width:100%; font-size:14px; margin-bottom:8px; }
table.reqtable th, table.reqtable td { padding:4px 6px; }
table.reqtable input, table.reqtable select { min-width: 70px; }
.remove-row { color: #d44; font-weight:bold; cursor:pointer; }
.add-row-btn { font-size: 13px; margin-top: 2px; }
summary { font-weight: bold; cursor:pointer; }
</style>
</head>
<body>
<div class="uk-container uk-padding">
<h1 class="uk-text-center uk-margin">Test Package Builder<br><span class="subtitle">Structured Table Entry UI</span></h1>
<form id="package-test-form"
hx-post="{% if package %}/{{ system }}/package/{{ package.id }}{% else %}/{{ system }}/package/new{% endif %}"
hx-trigger="submit"
hx-target="#toast-dock"
hx-swap="innerHTML"
method="POST" autocomplete="off">
{% csrf_token %}
<div class="uk-margin">
<label class="uk-form-label" for="package_name">Name</label>
<input class="uk-input" id="package_name" name="package_name" type="text" value="{{package.package_name|default:''}}" required>
</div>
<div class="uk-margin">
<label class="uk-form-label" for="package_type">Type</label>
<input class="uk-input" id="package_type" name="package_type" type="text" value="{{package.package_type|default:''}}">
</div>
<div class="uk-width-1-1">
<label class="uk-form-label" for="package_description">Description</label>
<div class="uk-form-controls">
<textarea class="uk-textarea" id="package_description" name="package_description" rows="3">{{package.package_description|default:''}}</textarea>
</div>
</div>
<div class="uk-width-1-1">
<label class="uk-form-label" for="package_doc_md">Documentation</label>
<div class="uk-form-controls">
<textarea class="uk-textarea" id="package_doc_md" name="package_doc_md" rows="3">{{package.package_doc_md|default:''}}</textarea>
</div>
</div>
<div class="uk-margin">
<div id="trait-dock"></div>
<input id="trait-ajax-autocomplete" class="uk-input uk-input-small">
<div id="trait-suggestions"></div>
</div>
<div class="uk-margin">
<label class="uk-form-label">Package Requirements</label>
<table id="reqtable" class="uk-table reqtable">
<thead>
<tr>
<th>Attribute</th>
<th>Condition</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for req in package.package_requirements %}
<tr>
<td><input class="uk-input" type="text" value="{{ req.attr }}"></td>
<td>
<select class="uk-select">
<option value=="==" {% if req.condition == "==" %}selected{% endif %}>=</option>
<option value=">=" {% if req.condition == ">=" %}selected{% endif %}>&ge;</option>
<option value="<=" {% if req.condition == "<=" %}selected{% endif %}>&le;</option>
<option value="!=" {% if req.condition == "!=" %}selected{% endif %}>&ne;</option>
</select>
</td>
<td><input class="uk-input" type="text" value="{{ req.value }}"></td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="button" class="uk-button uk-button-default uk-button-xsmall add-row-btn" id="add-package-req-btn" onclick="addPackageRequirement()">Add Requirement</button>
</div>
<div>
<ul uk-tab>
<li><a href="#">Operations</a></li>
<li><a href="#">Features</a></li>
</ul>
<div class="uk-switcher uk-margin">
<div>
<h3>Operations
<button id="add-operation-btn" type="button" class="uk-button uk-button-primary uk-button-small uk-margin-small-left">
<span uk-icon="icon: plus"></span> Add Operation
</button>
</h3>
<div id="operations-list"></div>
</div>
<div>
<button type="button" class="uk-button" onclick="openFeatureSelect()">Add Feature</button>
<table class="uk-table uk-table-striped">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody id="features-table-body">
</tbody>
</table>
</div>
</div>
</div>
<div class="uk-margin">
<button class="uk-button uk-button-primary uk-width-1-1">Submit Package</button>
</div>
</form>
</div>
<div id="featureSelectModal" class="uk-modal-container">
<div class="uk-modal-dialog uk-modal-body">
<h2 class="uk-modal-title">Select Feature...</h2>
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-margin">
<input onchange="changeFeatureSearchQuery()" id="feature-search-input" class="uk-input" placeholder="Search features...">
</div>
<div id="modal-feature-table-area">
<table class="uk-table uk-table-divider uk-table-hover">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody id="featureSearchTable">
</tbody>
</table>
<div id="featureSerachPage">
</div>
</div>
</div>
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script>
var operations = {{ package.package_operations|default:'[]'|safe }};
var selectedTraits = {{ traits|default:'[]'|safe }};
var selectedFeatures = {{ features|default:'[]'|safe}};
var system = "{{ system|escapejs }}";
console.log(operations)
renderOperations()
renderTraitChips()
renderFeatureTable(selectedFeatures)
UIkit.modal(document.getElementById('featureSelectModal'));
function openFeatureSelect(){
querySearchFeatures()
UIkit.modal(document.getElementById('featureSelectModal')).show()
}
function truncateWords(str, wordLimit) {
if (!str) return '';
const words = str.split(/\s+/);
return words.length > wordLimit
? words.slice(0, wordLimit).join(' ') + '…'
: str;
}
function renderFeatureTable(features){
let featureTable = document.getElementById('features-table-body');
featureTable.innerHTML = "";
features.forEach(feat => {
console.log(feat)
let tableRow = document.createElement('tr');
let nameCell = document.createElement('td');
nameCell.innerHTML = feat.feature_name;
let descriptionCell = document.createElement('td');
descriptionCell.innerHTML = truncateWords(feat.feature_description, 30)
let stateCell = document.createElement('td');
if(selectedFeatures.some(selfeat => selfeat.id === feat.id)){
console.log('true')
stateCell.innerHTML = `<span class="uk-label uk-label-success">Added</span>`
}
let actionCell = document.createElement('td');
tableRow.appendChild(nameCell)
tableRow.appendChild(descriptionCell)
tableRow.appendChild(stateCell)
tableRow.appendChild(actionCell)
featureTable.appendChild(tableRow)
});
}
// Functions to handle Features searching!
var currentFeaturePage = 1;
var endFeaturePage = 1;
var hasPrev = false;
var hasNext = false;
var searchFeatureQuery = "";
async function querySearchFeatures() {
const url = `/features/search/?system=${encodeURIComponent(system)}&q=${encodeURIComponent(searchFeatureQuery)}&page=${currentFeaturePage}`;
const response = await fetch(url)
const data = await response.json();
currentFeaturePage = data.page;
endFeaturePage = data.num_pages;
hasPrev = data.has_previous;
hasNext = data.has_next;
renderSearchFeatureTable(data.results);
renderSearchPagenation()
}
function changeFeatureSearchPage(page){
currentFeaturePage = page;
querySearchFeatures()
}
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
}
}
const debouncedFeatureQuery = debounce(function(event) {
const query = event.target.value.trim();
if(query.length < 2){
searchFeatureQuery = "";
querySearchFeatures()
return;
}
searchFeatureQuery = query;
currentFeaturePage = 1;
querySearchFeatures();
}, 250);
document.getElementById('feature-search-input').addEventListener('input', debouncedFeatureQuery);
function renderSearchFeatureTable(features){
console.log(features)
let featureTable = document.getElementById('featureSearchTable');
featureTable.innerHTML = "";
features.forEach(feat => {
console.log(feat)
let tableRow = document.createElement('tr');
let nameCell = document.createElement('td');
nameCell.innerHTML = feat.feature_name;
let descriptionCell = document.createElement('td');
descriptionCell.innerHTML = truncateWords(feat.feature_description, 30)
let actionCell = document.createElement('td');
actionCell.innerHTML = `<button class="uk-button uk-button-primary" type="button">Select</button>`
tableRow.appendChild(nameCell)
tableRow.appendChild(descriptionCell)
tableRow.appendChild(actionCell)
featureTable.appendChild(tableRow)
});
}
function renderSearchPagenation(){
let pageDiv = document.getElementById('featureSerachPage')
pageDiv.innerHTML = '';
let unorderedList = document.createElement('ul')
unorderedList.setAttribute('class', 'uk-pagination uk-flex-center')
let firstEl = document.createElement('li')
firstEl.innerHTML = `<a onclick="changeFeatureSearchPage(1)">First</a>`
let prevEl = document.createElement('li')
prevEl.innerHTML = `<a onclick="changeFeatureSearchPage(${currentFeaturePage - 1})">Prev</a>`
if(!hasPrev){
prevEl.setAttribute('class', 'uk-disabled')
prevEl.setAttribute('class', 'uk-disabled')
}
let pageEl = document.createElement('li')
pageEl.innerHTML = `<span>Page ${currentFeaturePage} of ${endFeaturePage}</span>`
let nextEl = document.createElement('li')
nextEl.innerHTML = `<a onclick="changeFeatureSearchPage(${currentFeaturePage + 1})">Next</a>`
let lastEl = document.createElement('li')
lastEl.innerHTML = `<a onclick="changeFeatureSearchPage(${endFeaturePage})">Last</a>`
if(!hasNext){
nextEl.setAttribute('class', 'uk-disabled')
lastEl.setAttribute('class', 'uk-disabled')
}
unorderedList.appendChild(firstEl)
unorderedList.appendChild(prevEl)
unorderedList.append(pageEl)
unorderedList.appendChild(nextEl)
unorderedList.appendChild(lastEl)
pageDiv.appendChild(unorderedList)
}
function renderTraitChips(){
let traitDock = document.getElementById('trait-dock');
traitDock.innerHTML = "";
selectedTraits.forEach(tr => {
let traitChip = document.createElement('span');
traitChip.className = "uk-label uk-label-primary uk-margin-small-right uk-margin-small-bottom";
traitChip.innerHTML = `${tr.trait_name}
<a href='#' style='color:white;margin-left:6px' onclick='removeTrait("${tr.id}");return false;'>&times;</a>`;
traitDock.appendChild(traitChip);
});
}
function addTrait(trait){
if (selectedTraits.some(t => t.id === trait.id)) return;
selectedTraits.push(trait)
renderTraitChips()
}
function removeTrait(id){
selectedTraits = selectedTraits.filter(t => String(t.id) !== String(id));
renderTraitChips()
}
document.getElementById('trait-ajax-autocomplete').addEventListener('input', function(){
let query = this.value.trim();
if(query.length < 2) return;
fetch(`/traits/search/?system={{ system }}&q=${encodeURIComponent(query)}`)
.then(resp => resp.json())
.then(data => {
let suggestionElement = document.getElementById('trait-suggestions')
suggestionElement.innerHTML = ''
if(data.length > 0){
suggestionElement.innerHTML = '<strong>Here are some suggestions: </strong>'
}
data.forEach(trait => {
let a_link = document.createElement('a')
a_link.href = "#";
a_link.innerHTML = trait.trait_name
a_link.onclick = function(e){
e.preventDefault();
addTrait(trait);
}
suggestionElement.appendChild(a_link)
suggestionElement.appendChild(document.createTextNode(" "));
});
suggestionElement.style.display = 'block';
});
});
function addPackageRequirement(){
var table = document.getElementById(`reqtable`);
var tbody = table.querySelector('tbody');
var tblRow = document.createElement('tr');
var attrCell = document.createElement('td');
var attrInput = document.createElement('input');
attrInput.type = 'text';
attrInput.className = 'uk-input';
attrCell.appendChild(attrInput);
tblRow.appendChild(attrCell);
var conditionCell = document.createElement('td');
var conditionSelect = document.createElement('select');
conditionSelect.className = 'uk-select';
['==','>=','<=','!='].forEach(function(opt) {
var option = document.createElement('option');
option.value = opt;
option.text = (opt === '==') ? '=' : opt;
conditionSelect.appendChild(option);
});
conditionCell.appendChild(conditionSelect);
tblRow.appendChild(conditionCell);
var valueCell = document.createElement('td');
var valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'uk-input';
valueCell.appendChild(valueInput);
tblRow.appendChild(valueCell);
tbody.appendChild(tblRow)
}
function makeOperationCard(operation, idx) {
console.log(operation)
const div = document.createElement('div');
div.className = 'operation-card uk-grid-small';
div.innerHTML += `<details ${operation._expanded ? 'open' : ''}>
<summary>Operation #${idx+1}: ${operation.attr || '—'}<span class='subtitle'> (${operation.operation||''})</span></summary>
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-3">
<label>Attribute
<input id="operation-attr${idx}" class="uk-input op-attribute" placeholder="Attribute" type="text" value="${operation.attr||''}">
</label>
</div>
<div class="uk-width-1-3">
<label>Operation
<select id="operation-operation${idx}" class="uk-select op-operation">
<option value="add" ${operation.operation==='add'?'selected':''}>Add</option>
<option value="subtract" ${operation.operation==='subtract'?'selected':''}>Subtract</option>
<option value="multiply" ${operation.operation==='multiply'?'selected':''}>Multiply</option>
<option value="divide" ${operation.operation==='divide'?'selected':''}>Divide</option>
<option value="set" ${operation.operation==='set'?'selected':''}>Set</option>
</select>
</label>
</div>
<div class="uk-width-1-3">
<label>Value
<input id="operation-value${idx}" class="uk-input op-value" type="text" value="${operation.value||''}">
</label>
</div>
</div>
<hr></hr>
<div class="uk-margin-small">
<div style="margin-top:6px;">
<table class="uk-table" id="limits-table-op${idx}">
<caption>Operations Limits</caption>
<thead>
<tr>
<th>Attribute</th>
<th>Condition</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" class="uk-button uk-button-default uk-width-1-2" onclick=addOperationLimits(${idx})>Add Limit</button>
</div>
<hr></hr>
<div style="margin-top:6px;">
<table class="uk-table uk-table-small" id="reqs-table-op${idx}">
<caption>Operations Requirements</caption>
<thead>
<tr>
<th>Attribute</th>
<th>Condition</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" class="uk-button uk-button-default uk-width-1-2" onclick=addOperationRequirement(${idx})>Add Requirement</button>
</div>
</div>
<button type="button" class="uk-button uk-button-danger uk-button-xsmall remove-op-btn"><span uk-icon="icon: close"></span> Remove Operation</button>
</details>`;
return div;
}
function addOperationLimits(idx){
var table = document.getElementById(`limits-table-op${idx}`);
var tbody = table.querySelector('tbody');
var tblRow = document.createElement('tr');
var attrCell = document.createElement('td');
var attrInput = document.createElement('input');
attrInput.type = 'text';
attrInput.className = 'uk-input';
attrCell.appendChild(attrInput);
tblRow.appendChild(attrCell);
var conditionCell = document.createElement('td');
var conditionSelect = document.createElement('select');
conditionSelect.className = 'uk-select';
['==','>=','<=','!='].forEach(function(opt) {
var option = document.createElement('option');
option.value = opt;
option.text = (opt === '==') ? '=' : opt;
conditionSelect.appendChild(option);
});
conditionCell.appendChild(conditionSelect);
tblRow.appendChild(conditionCell);
var valueCell = document.createElement('td');
var valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'uk-input';
valueCell.appendChild(valueInput);
tblRow.appendChild(valueCell);
tbody.appendChild(tblRow)
}
function addOperationRequirement(idx){
var table = document.getElementById(`reqs-table-op${idx}`);
var tbody = table.querySelector('tbody');
var tblRow = document.createElement('tr');
var attrCell = document.createElement('td');
var attrInput = document.createElement('input');
attrInput.type = 'text';
attrInput.className = 'uk-input';
attrCell.appendChild(attrInput);
tblRow.appendChild(attrCell);
var conditionCell = document.createElement('td');
var conditionSelect = document.createElement('select');
conditionSelect.className = 'uk-select';
['==','>=','<=','!='].forEach(function(opt) {
var option = document.createElement('option');
option.value = opt;
option.text = (opt === '==') ? '=' : opt;
conditionSelect.appendChild(option);
});
conditionCell.appendChild(conditionSelect);
tblRow.appendChild(conditionCell);
var valueCell = document.createElement('td');
var valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'uk-input';
valueCell.appendChild(valueInput);
tblRow.appendChild(valueCell);
tbody.appendChild(tblRow)
}
function readRequestTableRows(tableId){
const table = document.getElementById(tableId);
if(!table) return [];
const rows = table.querySelectorAll('tbody tr');
let arr = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if(cells.length>=3){
const attr = cells[0].querySelector('input')?.value || "";
const condition = cells[1].querySelector('select')?.value || "";
const value = cells[2].querySelector('input')?.value || "";
if(attr || condition || value){
arr.push({attr, condition, value});
}
}
})
return arr;
}
function renderOperations() {
const cont = document.getElementById('operations-list');
cont.innerHTML = '';
operations.forEach((op, i) => {
cont.appendChild(makeOperationCard(op, i));
});
}
document.getElementById('add-operation-btn').onclick = function() {
operations.push({ attribute: '', operation: '', value: '', limits: [], requirements: [], _expanded:true });
renderOperations();
};
document.getElementById('package-test-form').onsubmit = function(e) {
operations.forEach((op, idx) => {
op.attr = document.getElementById('operation-attr' + idx).value;
op.operation = document.getElementById('operation-operation' + idx).value;
op.value = document.getElementById('operation-value' + idx).value;
op.limits = readRequestTableRows('limits-table-op' + idx);
op.requirements = readRequestTableRows('reqs-table-op' + idx)
})
let package_requirements = readRequestTableRows('reqtable')
let payload = {
package_name: document.getElementById('package_name').value,
package_description: document.getElementById('package_description').value,
package_type: document.getElementById('package_type').value,
package_doc_md: document.getElementById('package_doc_md').value,
package_system: system,
package_requirements: package_requirements,
package_operations: operations.map(({_expanded, ...op})=>op),
package_traits: selectedTraits,
features: selectedFeatures
};
console.log(payload)
let field = document.getElementById('package_payload');
if (!field) {
field = document.createElement('input');
field.type = 'hidden';
field.id = 'package_payload';
field.name = 'package_payload';
this.appendChild(field);
}
field.value = JSON.stringify(payload);
};
</script>
<div id="toast-dock"></div>
</body>
</html>

View File

@ -3,7 +3,14 @@ from . import views
urlpatterns = [
path('', views.home, name='Home'),
path('feature', views.feature_add_view, name="feature_add"),
path('<str:system>/feature/new', views.feature_new, name="feature_new"),
path('<str:system>/feature/<uuid:feature_uuid>', views.feature_detail, name='feature_detail'),
path('<str:system>/features/', views.features_list, name="features_list"),
path('<str:system>/package/new', views.package_new, name="package_new"),
path('<str:system>/package/<uuid:package_uuid>', views.package_detail, name='package_detail'),
path('<str:system>/packages/', views.packages_list, name="packages_list"),
path('features/search/', views.feature_select_search, name="feature_select_search"),
path('traits/search/', views.trait_search, name="trait_search"),
path('map', views.map_page, name="Map"),
path('api/pins/', views.pin_list, name='pin_list'),
]

View File

@ -1,7 +1,8 @@
from django.shortcuts import render
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.apps import apps
from .models import Character, PackageFeature, Feature, Pin
from .models import Character, PackageFeature, Feature, Pin, Package, ObjectTrait
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from rest_framework.decorators import api_view
from rest_framework.response import Response
@ -12,6 +13,50 @@ from .serializers import PinSerializer
import json
# Create your views here.
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template.loader import render_to_string
from django.views.decorators.http import require_GET
@require_GET
def feature_select_search(request):
system = request.GET.get('system')
q = request.GET.get('q', '').strip()
page = request.GET.get('page', 1)
per_page = 5
print(q, system, page)
feats = Feature.objects.all()
if system:
feats = feats.filter(feature_system=system)
if q:
feats = feats.filter(
feature_name__icontains=q
)
paginator = Paginator(feats.order_by('feature_name'), per_page)
try:
page_obj = paginator.page(page)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
data = {
'results': [
{
'id': str(feat.id),
'feature_name': feat.feature_name,
'feature_description': feat.feature_description,
}
for feat in page_obj.object_list
],
'page': page_obj.number,
'num_pages': paginator.num_pages,
'has_previous': page_obj.has_previous(),
'has_next': page_obj.has_next(),
}
return JsonResponse(data)
def home(request):
with open('test_character.json', 'r+') as file:
character = json.load(file)
@ -25,69 +70,170 @@ def home(request):
)
character.char_class.display_features = class_feature_links
return render(request, 'main/character_sheet.html', {'character': character, })
return render(request, 'main/character_sheet.html')
def feature_add_view(request):
def package_detail(request, system, package_uuid):
package = get_object_or_404(Package, id=package_uuid)
if request.method == 'POST':
try:
payload = json.loads(request.POST.get('package_payload'))
package.package_name = payload.get('package_name')
package.package_description = payload.get('package_description')
package.package_requirements = payload.get('package_requirements')
package.package_doc_md = payload.get('package_doc_md')
package.package_operations = payload.get('package_operations')
package.package_type = payload.get('package_type')
trait_ids = [trait['id'] for trait in payload.get('package_traits', [])]
print(trait_ids)
if trait_ids is not None:
package.package_traits.set(trait_ids)
feat_ids = [feat['id'] for feat in payload.get('features', [])]
print(feat_ids)
if trait_ids is not None:
package.features.set(feat_ids)
package.save()
return HttpResponse('''
<script>
UIkit.notification({message: "Feature Updated Successfully!", status: "success", pos: "top-center"})
</script>
''')
except Exception as e:
return HttpResponse('''
<script>
UIkit.notification({message: "Feature Update Unsuccessful, there appears to have been an error!", status: "danger", pos: "top-center"})
</script>
''')
elif request.method == 'GET':
traits = package.package_traits.all()
traits = [trait.to_json() for trait in traits]
features = package.features.all()
features = [feat.to_json() for feat in features]
return render(request, 'main/test_package.html', {'system': system, 'package': package, 'traits': traits, 'features':features })
def package_new(request, system):
if request.method == 'GET':
return render(request, 'main/feature.html')
return render(request, 'main/test_package.html', {'system': system})
elif request.method == 'POST':
# Parse basic fields
name = request.POST.get('feature_name')
description = request.POST.get('feature_description')
payload_json = request.POST.get('package_payload')
try:
payload = json.loads(payload_json)
print('---PAYLOAD---')
print(json.dumps(payload, indent=2))
feature = Feature.objects.create(
feature_name=payload.get('feature_name'),
feature_description=payload.get('feature_description'),
feature_system=str(system),
feature_requirements=payload.get('feature_requirements'),
feature_data=payload.get('feature_data')
)
# Parse Requirements
requirements = []
req_keys = [k for k in request.POST.keys() if k.startswith('requirement_')]
idxs = set()
for k in req_keys:
try:
idxs.add(int(k.split('_')[-1]))
except:
continue
for idx in sorted(list(idxs)):
prop = request.POST.get(f'requirement_property_{idx}', '').strip()
cond = request.POST.get(f'requirement_condition_{idx}', '').strip()
val = request.POST.get(f'requirement_value_{idx}', '').strip()
if prop and cond and val:
requirements.append({'property': prop, 'condition': cond, 'value': val})
trait_ids = [trait['id'] for trait in payload.get('feature_traits', [])]
# Parse Operations (with extra subparts)
operations = []
op_keys = [k for k in request.POST.keys() if k.startswith('operation_attr_')]
op_idxs = [int(k.replace('operation_attr_', '')) for k in op_keys]
for op_idx in op_idxs:
attr = request.POST.get(f'operation_attr_{op_idx}', '').strip()
op = request.POST.get(f'operation_operation_{op_idx}', '').strip()
value = request.POST.get(f'operation_value_{op_idx}', '').strip()
# Subparts/limits
subparts = []
sub_idx = 0
while True:
kfield = f'operation_{op_idx}_limitkey_{sub_idx}'
vfield = f'operation_{op_idx}_limitval_{sub_idx}'
if kfield in request.POST and vfield in request.POST:
k = request.POST.get(kfield, '').strip()
v = request.POST.get(vfield, '').strip()
if k and v:
subparts.append({'key': k, 'value': v})
sub_idx += 1
else:
break
operation = {'attr': attr, 'operation': op, 'value': value}
for part in subparts:
operation[part['key']] = part['value']
operations.append(operation)
if trait_ids is not None:
feature.feature_traits.set(trait_ids)
response = HttpResponse(status=204)
response['HX-Redirect'] = f'/{system}/feature/{feature.id}'
return response
except Exception as e:
print('Failed to parse payload:', str(e))
payload = None
feat = Feature.objects.create(
feature_name=name,
feature_description=description,
feature_requirements=requirements,
feature_data={'operations': operations}
)
return render(request, 'main/feature_add.html', {
'msg': f'Feature "{feat.feature_name}" added successfully!',
})
return HttpResponse('<div class="uk-alert-success">Payload printed to server log.</div>', content_type="text/html")
def packages_list(request, system):
packages = Package.objects.filter(package_system=system)
return render(request, "main/packages_list.html", {
"packages": packages,
"system": system
})
def feature_detail(request, system, feature_uuid):
feature = get_object_or_404(Feature, id=feature_uuid)
if request.method == 'POST':
try:
payload = json.loads(request.POST.get('feature_payload'))
feature.feature_name = payload.get('feature_name')
feature.feature_description = payload.get('feature_description')
feature.feature_requirements = payload.get('feature_requirements')
feature.feature_data = payload.get('feature_data')
feature.save()
trait_ids = [trait['id'] for trait in payload.get('feature_traits', [])]
print(trait_ids)
if trait_ids is not None:
feature.feature_traits.set(trait_ids)
return HttpResponse('''
<script>
UIkit.notification({message: "Feature Updated Successfully!", status: "success", pos: "top-center"})
</script>
''')
except Exception as e:
return HttpResponse('''
<script>
UIkit.notification({message: "Feature Update Unsuccessful, there appears to have been an error!", status: "danger", pos: "top-center"})
</script>
''')
elif request.method == 'GET':
traits = feature.feature_traits.all()
traits = [trait.to_json() for trait in traits]
return render(request, 'main/test_feature.html', {'system': system, 'feature': feature, 'traits': traits })
def feature_new(request, system):
if request.method == 'GET':
return render(request, 'main/test_feature.html', {'system': system})
elif request.method == 'POST':
payload_json = request.POST.get('feature_payload')
try:
payload = json.loads(payload_json)
print('---PAYLOAD---')
print(json.dumps(payload, indent=2))
feature = Feature.objects.create(
feature_name=payload.get('feature_name'),
feature_description=payload.get('feature_description'),
feature_system=str(system),
feature_requirements=payload.get('feature_requirements'),
feature_data=payload.get('feature_data')
)
trait_ids = [trait['id'] for trait in payload.get('feature_traits', [])]
if trait_ids is not None:
feature.feature_traits.set(trait_ids)
response = HttpResponse(status=204)
response['HX-Redirect'] = f'/{system}/feature/{feature.id}'
return response
except Exception as e:
print('Failed to parse payload:', str(e))
payload = None
return HttpResponse('<div class="uk-alert-success">Payload printed to server log.</div>', content_type="text/html")
def features_list(request, system):
features = Feature.objects.filter(feature_system=system)
return render(request, "main/features_list.html", {
"features": features,
"system": system
})
def map_page(request):
return render(request, "main/map.html")
@ -104,3 +250,9 @@ def pin_list(request):
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def trait_search(request):
query = request.GET.get('q', '')
system = request.GET.get('system', '')
traits = ObjectTrait.objects.filter(trait_system=system, trait_name__icontains=query).values('id', 'trait_name')[:20]
return JsonResponse(list(traits), safe=False)