second commit
This commit is contained in:
parent
c1ad629e79
commit
adf7f20e58
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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',)
|
||||
@ -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')),
|
||||
],
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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)')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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' : ''}>=</option>
|
||||
<option value=">=" ${cond=='>=' ? 'selected' : ''}>≥</option>
|
||||
<option value="<=" ${cond=='<=' ? 'selected' : ''}>≤</option>
|
||||
<option value="!=" ${cond=='!=' ? 'selected' : ''}>≠</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' : ''}>=</option>
|
||||
<option value=">=" ${cond=='>=' ? 'selected' : ''}>≥</option>
|
||||
<option value="<=" ${cond=='<=' ? 'selected' : ''}>≤</option>
|
||||
<option value="!=" ${cond=='!=' ? 'selected' : ''}>≠</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' : ''}>=</option>
|
||||
<option value=">=" ${cond=='>=' ? 'selected' : ''}>≥</option>
|
||||
<option value="<=" ${cond=='<=' ? 'selected' : ''}>≤</option>
|
||||
<option value="!=" ${cond=='!=' ? 'selected' : ''}>≠</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' : ''}>=</option>
|
||||
<option value=">=" ${cond=='>=' ? 'selected' : ''}>≥</option>
|
||||
<option value="<=" ${cond=='<=' ? 'selected' : ''}>≤</option>
|
||||
<option value="!=" ${cond=='!=' ? 'selected' : ''}>≠</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>
|
||||
</html>
|
||||
|
||||
78
main/templates/main/feature_detail.html
Normal file
78
main/templates/main/feature_detail.html
Normal 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 %}>≥</option>
|
||||
<option value="<=" {% if req.condition == "<=" %}selected{% endif %}>≤</option>
|
||||
<option value="!=" {% if req.condition == "!=" %}selected{% endif %}>≠</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>
|
||||
49
main/templates/main/feature_select_table.html
Normal file
49
main/templates/main/feature_select_table.html
Normal 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">« 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>« 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 »</a></li>
|
||||
{% else %}
|
||||
<li class="uk-disabled"><span>Next</span></li>
|
||||
<li class="uk-disabled"><span>Last »</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
47
main/templates/main/features_list.html
Normal file
47
main/templates/main/features_list.html
Normal 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>
|
||||
49
main/templates/main/packages_list.html
Normal file
49
main/templates/main/packages_list.html
Normal 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>
|
||||
475
main/templates/main/test_feature.html
Normal file
475
main/templates/main/test_feature.html
Normal 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 %}>≥</option>
|
||||
<option value="<=" {% if req.condition == "<=" %}selected{% endif %}>≤</option>
|
||||
<option value="!=" {% if req.condition == "!=" %}selected{% endif %}>≠</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;'>×</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>
|
||||
621
main/templates/main/test_package.html
Normal file
621
main/templates/main/test_package.html
Normal 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 %}>≥</option>
|
||||
<option value="<=" {% if req.condition == "<=" %}selected{% endif %}>≤</option>
|
||||
<option value="!=" {% if req.condition == "!=" %}selected{% endif %}>≠</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;'>×</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>
|
||||
@ -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'),
|
||||
]
|
||||
270
main/views.py
270
main/views.py
@ -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")
|
||||
@ -103,4 +249,10 @@ def pin_list(request):
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user