From bdcc4ac4c34a9330da87d125e28ada6f0918639b Mon Sep 17 00:00:00 2001 From: Brent Mills Date: Mon, 18 Nov 2024 19:37:06 -0500 Subject: [PATCH 1/8] Create relocation_planner --- config/settings/base.py | 1 + config/urls.py | 1 + .../pack_planner/assessment_results.html | 3 +- relocation_planner/__init__.py | 0 relocation_planner/admin.py | 3 + relocation_planner/apps.py | 6 + relocation_planner/migrations/0001_initial.py | 310 ++++++++++++++++++ relocation_planner/migrations/__init__.py | 0 relocation_planner/models.py | 260 +++++++++++++++ .../relocation_planner/country_list.html | 87 +++++ .../templates/relocation_planner/landing.html | 43 +++ relocation_planner/tests.py | 3 + relocation_planner/urls.py | 9 + relocation_planner/views.py | 55 ++++ 14 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 relocation_planner/__init__.py create mode 100644 relocation_planner/admin.py create mode 100644 relocation_planner/apps.py create mode 100644 relocation_planner/migrations/0001_initial.py create mode 100644 relocation_planner/migrations/__init__.py create mode 100644 relocation_planner/models.py create mode 100644 relocation_planner/templates/relocation_planner/country_list.html create mode 100644 relocation_planner/templates/relocation_planner/landing.html create mode 100644 relocation_planner/tests.py create mode 100644 relocation_planner/urls.py create mode 100644 relocation_planner/views.py diff --git a/config/settings/base.py b/config/settings/base.py index 241f870..68f894b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -34,6 +34,7 @@ 'companies.apps.CompaniesConfig', 'online_security.apps.OnlineSecurityConfig', 'pack_planner.apps.PackPlannerConfig', + 'relocation_planner.apps.RelocationPlannerConfig', 'safety_at_home.apps.SafetyAtHomeConfig', 'users.apps.UsersConfig', ] diff --git a/config/urls.py b/config/urls.py index 8c3e6d8..ba10dbd 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,6 +9,7 @@ path('login/', auth_views.LoginView.as_view(template_name='companies/login.html'), name='login'), path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'), path('packplanner/', include('pack_planner.urls')), + path('relocation/', include('relocation_planner.urls')), path('safetyathome/', include('safety_at_home.urls')), path('securityonline/', include('online_security.urls')), ] diff --git a/pack_planner/templates/pack_planner/assessment_results.html b/pack_planner/templates/pack_planner/assessment_results.html index 731234a..a85f06a 100644 --- a/pack_planner/templates/pack_planner/assessment_results.html +++ b/pack_planner/templates/pack_planner/assessment_results.html @@ -244,7 +244,7 @@

{{ category.name }}

- View Details + More Info @@ -382,6 +382,7 @@

{{ category.name }}

}) .then(response => response.json()) .then(data => { + console.log('Full response data:', data); if (data.status === 'success') { const itemElement = buttonElement.closest('[data-item-id]'); itemElement.classList.add('hidden'); diff --git a/relocation_planner/__init__.py b/relocation_planner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/relocation_planner/admin.py b/relocation_planner/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/relocation_planner/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/relocation_planner/apps.py b/relocation_planner/apps.py new file mode 100644 index 0000000..da9cc4c --- /dev/null +++ b/relocation_planner/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RelocationPlannerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'relocation_planner' diff --git a/relocation_planner/migrations/0001_initial.py b/relocation_planner/migrations/0001_initial.py new file mode 100644 index 0000000..bd3df4d --- /dev/null +++ b/relocation_planner/migrations/0001_initial.py @@ -0,0 +1,310 @@ +# Generated by Django 5.1.3 on 2024-11-18 23:01 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ('currency', models.CharField(max_length=50)), + ('capital_city', models.CharField(max_length=100)), + ('population', models.PositiveIntegerField()), + ('timezone', models.CharField(max_length=50)), + ('avg_cost_of_living', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('avg_house_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('avg_rent_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'countries', + }, + ), + migrations.CreateModel( + name='CostOfLiving', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('city', models.CharField(max_length=100)), + ('date_recorded', models.DateField()), + ('rent_1bed_city', models.DecimalField(decimal_places=2, max_digits=10)), + ('rent_3bed_city', models.DecimalField(decimal_places=2, max_digits=10)), + ('rent_1bed_suburban', models.DecimalField(decimal_places=2, max_digits=10)), + ('rent_3bed_suburban', models.DecimalField(decimal_places=2, max_digits=10)), + ('utilities', models.DecimalField(decimal_places=2, max_digits=8)), + ('internet', models.DecimalField(decimal_places=2, max_digits=8)), + ('groceries', models.DecimalField(decimal_places=2, max_digits=8)), + ('transportation', models.DecimalField(decimal_places=2, max_digits=8)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ], + options={ + 'verbose_name_plural': 'costs of living', + }, + ), + migrations.CreateModel( + name='CitizenshipProcess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('residence_requirement_years', models.PositiveIntegerField()), + ('language_requirement', models.TextField()), + ('dual_citizenship_allowed', models.BooleanField()), + ('test_required', models.BooleanField()), + ('test_details', models.TextField(blank=True)), + ('cost', models.DecimalField(decimal_places=2, max_digits=10)), + ('process_details', models.TextField()), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EditHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('object_id', models.PositiveIntegerField()), + ('field_name', models.CharField(max_length=100)), + ('old_value', models.TextField(blank=True)), + ('new_value', models.TextField(blank=True)), + ('change_reason', models.TextField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HealthcareInfo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('has_universal_healthcare', models.BooleanField()), + ('immigrant_coverage_waiting_period', models.CharField(max_length=100)), + ('private_insurance_requirement', models.TextField()), + ('avg_cost_private_insurance', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)), + ('healthcare_system_details', models.TextField()), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Language', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=50, unique=True)), + ('code', models.CharField(max_length=10, unique=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='country', + name='business_language', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='business_countries', to='relocation_planner.language'), + ), + migrations.AddField( + model_name='country', + name='official_languages', + field=models.ManyToManyField(related_name='countries', to='relocation_planner.language'), + ), + migrations.CreateModel( + name='PetRelocationRequirement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('pet_type', models.CharField(max_length=50)), + ('quarantine_required', models.BooleanField()), + ('quarantine_duration', models.CharField(blank=True, max_length=50)), + ('vaccination_requirements', models.TextField()), + ('documentation_required', models.TextField()), + ('restrictions', models.TextField(blank=True)), + ('estimated_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PropertyCost', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('city', models.CharField(max_length=100)), + ('date_recorded', models.DateField()), + ('property_type', models.CharField(choices=[('APARTMENT', 'Apartment'), ('HOUSE', 'House'), ('LAND', 'Land')], max_length=50)), + ('size_sqm', models.DecimalField(decimal_places=2, max_digits=8)), + ('price', models.DecimalField(decimal_places=2, max_digits=12)), + ('location_type', models.CharField(choices=[('CITY_CENTER', 'City Center'), ('SUBURBAN', 'Suburban'), ('RURAL', 'Rural')], max_length=20)), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ResourceLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=200)), + ('url', models.URLField()), + ('category', models.CharField(choices=[('OFFICIAL', 'Official Government Resource'), ('HEALTHCARE', 'Healthcare Information'), ('HOUSING', 'Housing Information'), ('EDUCATION', 'Education Information'), ('VISA', 'Visa Information'), ('EMPLOYMENT', 'Employment Information'), ('COMMUNITY', 'Community Resource')], max_length=50)), + ('description', models.TextField()), + ('is_verified', models.BooleanField(default=False)), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='UserExperience', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=200)), + ('content', models.TextField()), + ('moved_from', models.CharField(max_length=100)), + ('year_moved', models.PositiveIntegerField()), + ('current_status', models.CharField(choices=[('PLANNING', 'Planning Stage'), ('IN_PROGRESS', 'Currently Relocating'), ('RELOCATED', 'Successfully Relocated'), ('RETURNED', 'Returned to Origin')], max_length=50)), + ('would_recommend', models.BooleanField()), + ('challenges_faced', models.TextField()), + ('tips', models.TextField()), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='VisaType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100)), + ('description', models.TextField()), + ('duration', models.CharField(max_length=50)), + ('processing_time', models.CharField(max_length=50)), + ('allows_family', models.BooleanField()), + ('family_notes', models.TextField(blank=True)), + ('can_work', models.BooleanField()), + ('path_to_citizenship', models.BooleanField()), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('country', 'name')}, + }, + ), + migrations.CreateModel( + name='VisaRequirement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('requirement', models.CharField(max_length=255)), + ('details', models.TextField()), + ('is_mandatory', models.BooleanField(default=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ('visa_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.visatype')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Rating', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.CharField(choices=[('SAFETY', 'Safety'), ('HEALTHCARE', 'Healthcare Quality'), ('EDUCATION', 'Education Quality'), ('HAPPINESS', 'Overall Happiness'), ('INFRASTRUCTURE', 'Infrastructure'), ('FOREIGNER_FRIENDLY', 'Foreigner Friendliness'), ('WORK_LIFE', 'Work-Life Balance'), ('ENVIRONMENT', 'Environmental Quality')], max_length=50)), + ('score', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])), + ('comment', models.TextField(blank=True)), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('country', 'user', 'category')}, + }, + ), + migrations.CreateModel( + name='RelocationProcess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('step_number', models.PositiveIntegerField()), + ('title', models.CharField(max_length=200)), + ('description', models.TextField()), + ('estimated_time', models.CharField(max_length=50)), + ('estimated_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('required_documents', models.TextField()), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['step_number'], + 'unique_together': {('country', 'step_number')}, + }, + ), + ] diff --git a/relocation_planner/migrations/__init__.py b/relocation_planner/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/relocation_planner/models.py b/relocation_planner/models.py new file mode 100644 index 0000000..526b0a3 --- /dev/null +++ b/relocation_planner/models.py @@ -0,0 +1,260 @@ +from django.db import models +from django.core.validators import MinValueValidator, MaxValueValidator +from django.contrib.auth.models import User +from django.utils.text import slugify +from django.conf import settings + +class BaseModel(models.Model): + """Abstract base model with common fields""" + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name="%(class)s_created" + ) + last_modified_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name="%(class)s_modified" + ) + + class Meta: + abstract = True + +class Country(BaseModel): + """Core country information""" + name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=100, unique=True) + official_languages = models.ManyToManyField('Language', related_name='countries') + business_language = models.ForeignKey( + 'Language', + on_delete=models.SET_NULL, + null=True, + related_name='business_countries' + ) + currency = models.CharField(max_length=50) + capital_city = models.CharField(max_length=100) + population = models.PositiveIntegerField() + timezone = models.CharField(max_length=50) + + # Automatically updated statistics + avg_cost_of_living = models.DecimalField( + max_digits=10, + decimal_places=2, + null=True, + blank=True + ) + avg_house_cost = models.DecimalField( + max_digits=12, + decimal_places=2, + null=True, + blank=True + ) + avg_rent_cost = models.DecimalField( + max_digits=10, + decimal_places=2, + null=True, + blank=True + ) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + class Meta: + verbose_name_plural = "countries" + +class Language(BaseModel): + """Language information""" + name = models.CharField(max_length=50, unique=True) + code = models.CharField(max_length=10, unique=True) # ISO code + +class Rating(BaseModel): + """User ratings for various country aspects""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + category = models.CharField(max_length=50, choices=[ + ('SAFETY', 'Safety'), + ('HEALTHCARE', 'Healthcare Quality'), + ('EDUCATION', 'Education Quality'), + ('HAPPINESS', 'Overall Happiness'), + ('INFRASTRUCTURE', 'Infrastructure'), + ('FOREIGNER_FRIENDLY', 'Foreigner Friendliness'), + ('WORK_LIFE', 'Work-Life Balance'), + ('ENVIRONMENT', 'Environmental Quality') + ]) + score = models.IntegerField( + validators=[MinValueValidator(1), MaxValueValidator(5)] + ) + comment = models.TextField(blank=True) + + class Meta: + unique_together = ['country', 'user', 'category'] + +class VisaType(BaseModel): + """Different types of visas available""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + name = models.CharField(max_length=100) + description = models.TextField() + duration = models.CharField(max_length=50) + processing_time = models.CharField(max_length=50) + allows_family = models.BooleanField() + family_notes = models.TextField(blank=True) + can_work = models.BooleanField() + path_to_citizenship = models.BooleanField() + + class Meta: + unique_together = ['country', 'name'] + +class VisaRequirement(BaseModel): + """Requirements for specific visa types""" + visa_type = models.ForeignKey(VisaType, on_delete=models.CASCADE) + requirement = models.CharField(max_length=255) + details = models.TextField() + is_mandatory = models.BooleanField(default=True) + +class CostOfLiving(BaseModel): + """User-submitted cost of living data""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + city = models.CharField(max_length=100) + date_recorded = models.DateField() + + # Monthly costs + rent_1bed_city = models.DecimalField(max_digits=10, decimal_places=2) + rent_3bed_city = models.DecimalField(max_digits=10, decimal_places=2) + rent_1bed_suburban = models.DecimalField(max_digits=10, decimal_places=2) + rent_3bed_suburban = models.DecimalField(max_digits=10, decimal_places=2) + utilities = models.DecimalField(max_digits=8, decimal_places=2) + internet = models.DecimalField(max_digits=8, decimal_places=2) + groceries = models.DecimalField(max_digits=8, decimal_places=2) + transportation = models.DecimalField(max_digits=8, decimal_places=2) + + class Meta: + verbose_name_plural = "costs of living" + +class PropertyCost(BaseModel): + """User-submitted property cost data""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + city = models.CharField(max_length=100) + date_recorded = models.DateField() + property_type = models.CharField(max_length=50, choices=[ + ('APARTMENT', 'Apartment'), + ('HOUSE', 'House'), + ('LAND', 'Land') + ]) + size_sqm = models.DecimalField(max_digits=8, decimal_places=2) + price = models.DecimalField(max_digits=12, decimal_places=2) + location_type = models.CharField(max_length=20, choices=[ + ('CITY_CENTER', 'City Center'), + ('SUBURBAN', 'Suburban'), + ('RURAL', 'Rural') + ]) + +class RelocationProcess(BaseModel): + """Documentation of relocation processes""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + step_number = models.PositiveIntegerField() + title = models.CharField(max_length=200) + description = models.TextField() + estimated_time = models.CharField(max_length=50) + estimated_cost = models.DecimalField( + max_digits=10, + decimal_places=2, + null=True, + blank=True + ) + required_documents = models.TextField() + + class Meta: + unique_together = ['country', 'step_number'] + ordering = ['step_number'] + +class PetRelocationRequirement(BaseModel): + """Requirements for relocating with pets""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + pet_type = models.CharField(max_length=50) # dog, cat, bird, etc. + quarantine_required = models.BooleanField() + quarantine_duration = models.CharField(max_length=50, blank=True) + vaccination_requirements = models.TextField() + documentation_required = models.TextField() + restrictions = models.TextField(blank=True) + estimated_cost = models.DecimalField( + max_digits=8, + decimal_places=2, + null=True, + blank=True + ) + +class CitizenshipProcess(BaseModel): + """Documentation of naturalization processes""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + residence_requirement_years = models.PositiveIntegerField() + language_requirement = models.TextField() + dual_citizenship_allowed = models.BooleanField() + test_required = models.BooleanField() + test_details = models.TextField(blank=True) + cost = models.DecimalField(max_digits=10, decimal_places=2) + process_details = models.TextField() + +class HealthcareInfo(BaseModel): + """Healthcare system information""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + has_universal_healthcare = models.BooleanField() + immigrant_coverage_waiting_period = models.CharField(max_length=100) + private_insurance_requirement = models.TextField() + avg_cost_private_insurance = models.DecimalField( + max_digits=8, + decimal_places=2, + null=True, + blank=True + ) + healthcare_system_details = models.TextField() + +class ResourceLink(BaseModel): + """External resources and references""" + country = models.ForeignKey(Country, on_delete=models.CASCADE) + title = models.CharField(max_length=200) + url = models.URLField() + category = models.CharField(max_length=50, choices=[ + ('OFFICIAL', 'Official Government Resource'), + ('HEALTHCARE', 'Healthcare Information'), + ('HOUSING', 'Housing Information'), + ('EDUCATION', 'Education Information'), + ('VISA', 'Visa Information'), + ('EMPLOYMENT', 'Employment Information'), + ('COMMUNITY', 'Community Resource') + ]) + description = models.TextField() + is_verified = models.BooleanField(default=False) + +class UserExperience(BaseModel): + """User experiences and stories""" + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + country = models.ForeignKey(Country, on_delete=models.CASCADE) + title = models.CharField(max_length=200) + content = models.TextField() + moved_from = models.CharField(max_length=100) + year_moved = models.PositiveIntegerField() + current_status = models.CharField(max_length=50, choices=[ + ('PLANNING', 'Planning Stage'), + ('IN_PROGRESS', 'Currently Relocating'), + ('RELOCATED', 'Successfully Relocated'), + ('RETURNED', 'Returned to Origin') + ]) + would_recommend = models.BooleanField() + challenges_faced = models.TextField() + tips = models.TextField() + +class EditHistory(BaseModel): + """Track changes to content""" + content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + field_name = models.CharField(max_length=100) + old_value = models.TextField(blank=True) + new_value = models.TextField(blank=True) + change_reason = models.TextField() + \ No newline at end of file diff --git a/relocation_planner/templates/relocation_planner/country_list.html b/relocation_planner/templates/relocation_planner/country_list.html new file mode 100644 index 0000000..f14596d --- /dev/null +++ b/relocation_planner/templates/relocation_planner/country_list.html @@ -0,0 +1,87 @@ +{# relocation_planner/templates/relocation_planner/country_list.html #} +{% extends 'relocation_planner/base.html' %} + +{% block relocation_content %} +
+ {# Search and Filter Section #} +
+
+
+ + +
+ + {% if request.GET.search %} + + Clear + + {% endif %} +
+
+ + {# Countries Grid #} +
+ {% for country in countries %} +
+

+ + {{ country.name }} + +

+ +
+

Capital: {{ country.capital_city }}

+

Population: {{ country.population|intcomma }}

+ + {% if country.official_languages.exists %} +

Languages: + {{ country.official_languages.all|join:", " }} +

+ {% endif %} + + {% if country.avg_cost_of_living %} +

Avg. Cost of Living: ${{ country.avg_cost_of_living|floatformat:2 }}

+ {% endif %} +
+ + +
+ {% empty %} +
+ No countries found. + {% if user.is_authenticated %} + + Add one? + + {% endif %} +
+ {% endfor %} +
+ + {# Add Country Button #} + {% if user.is_authenticated %} + + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/relocation_planner/templates/relocation_planner/landing.html b/relocation_planner/templates/relocation_planner/landing.html new file mode 100644 index 0000000..1d64bcd --- /dev/null +++ b/relocation_planner/templates/relocation_planner/landing.html @@ -0,0 +1,43 @@ +{% extends 'Users/base.html' %} + +{% block title %}Relocation Planner - {{ block.super }}{% endblock %} + +{% block content %} +{# Sub-navigation #} +
+ +
+ +
+ {# Hero Section #} +
+

Find Your New Home

+

Explore countries, compare options, and plan your international relocation

+ + {% if not user.is_authenticated %} + + Sign in to contribute + + {% endif %} +
+ + {# Rest of the template content remains the same #} + ... +
+{% endblock %} \ No newline at end of file diff --git a/relocation_planner/tests.py b/relocation_planner/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/relocation_planner/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/relocation_planner/urls.py b/relocation_planner/urls.py new file mode 100644 index 0000000..f9a1219 --- /dev/null +++ b/relocation_planner/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = 'relocation_planner' + +urlpatterns = [ + path('', views.LandingView.as_view(), name='landing'), + path('countries/', views.CountryListView.as_view(), name='country-list'), +] \ No newline at end of file diff --git a/relocation_planner/views.py b/relocation_planner/views.py new file mode 100644 index 0000000..0c1f4b4 --- /dev/null +++ b/relocation_planner/views.py @@ -0,0 +1,55 @@ +from django.views.generic import ListView, TemplateView +from django.db.models import Avg, Count +from .models import Country, UserExperience, Rating + +class LandingView(TemplateView): + template_name = 'relocation_planner/landing.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Get featured countries - for now, selecting countries with the most data + # We could modify this logic based on your preferences + featured_countries = Country.objects.annotate( + rating_count=Count('rating'), + experience_count=Count('userexperience'), + data_richness=Count('rating') + Count('userexperience') + ).order_by('-data_richness')[:6] # Adjust number as needed + + # Get recent experiences + recent_experiences = UserExperience.objects.select_related( + 'user', 'country' + ).order_by('-created_at')[:5] + + context.update({ + 'featured_countries': featured_countries, + 'recent_experiences': recent_experiences, + }) + return context + +class CountryListView(ListView): + model = Country + template_name = 'relocation_planner/country_list.html' + context_object_name = 'countries' + paginate_by = 12 # Shows 12 countries per page + + def get_queryset(self): + queryset = Country.objects.annotate( + avg_rating=Avg('rating__score') + ).select_related( + 'business_language' + ).prefetch_related( + 'official_languages' + ) + + # Handle search + search_query = self.request.GET.get('search', '').strip() + if search_query: + queryset = queryset.filter(name__icontains=search_query) + + return queryset.order_by('name') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['search_query'] = self.request.GET.get('search', '') + return context \ No newline at end of file From 09cf882564188b77ff70cb5fb32c631f77709f5b Mon Sep 17 00:00:00 2001 From: Brent Mills Date: Fri, 22 Nov 2024 11:29:37 -0500 Subject: [PATCH 2/8] Partially functional country_edit template and forms. It still needs work to get the JS fully functional and get around the id issues I'm seeing --- relocation_planner/admin.py | 3 - relocation_planner/forms/__init__.py | 13 + relocation_planner/forms/edit_country.py | 21 + .../forms/edit_pet_relocation_data.py | 7 + relocation_planner/forms/edit_visa.py | 9 + .../forms/edit_visa_requirement.py | 7 + relocation_planner/forms/visa_formset.py | 20 + ...02_remove_costofliving_country_and_more.py | 361 ++++++++++++++++ ...visas_remove_visa_requirements_and_more.py | 34 ++ relocation_planner/models.py | 283 ++++--------- .../relocation_planner/assessment.html | 24 ++ .../assessment_results.html | 24 ++ .../templates/relocation_planner/browse.html | 34 ++ .../relocation_planner/country_detail.html | 184 +++++++++ .../relocation_planner/country_list.html | 87 ---- .../relocation_planner/edit_country.html | 248 +++++++++++ .../templates/relocation_planner/landing.html | 57 +-- relocation_planner/templatetags/__init__.py | 0 .../templatetags/dict_filters.py | 7 + relocation_planner/urls.py | 19 +- relocation_planner/views.py | 384 +++++++++++++++--- 21 files changed, 1436 insertions(+), 390 deletions(-) create mode 100644 relocation_planner/forms/__init__.py create mode 100644 relocation_planner/forms/edit_country.py create mode 100644 relocation_planner/forms/edit_pet_relocation_data.py create mode 100644 relocation_planner/forms/edit_visa.py create mode 100644 relocation_planner/forms/edit_visa_requirement.py create mode 100644 relocation_planner/forms/visa_formset.py create mode 100644 relocation_planner/migrations/0002_remove_costofliving_country_and_more.py create mode 100644 relocation_planner/migrations/0003_remove_country_visas_remove_visa_requirements_and_more.py create mode 100644 relocation_planner/templates/relocation_planner/assessment.html create mode 100644 relocation_planner/templates/relocation_planner/assessment_results.html create mode 100644 relocation_planner/templates/relocation_planner/browse.html create mode 100644 relocation_planner/templates/relocation_planner/country_detail.html delete mode 100644 relocation_planner/templates/relocation_planner/country_list.html create mode 100644 relocation_planner/templates/relocation_planner/edit_country.html create mode 100644 relocation_planner/templatetags/__init__.py create mode 100644 relocation_planner/templatetags/dict_filters.py diff --git a/relocation_planner/admin.py b/relocation_planner/admin.py index 8c38f3f..e69de29 100644 --- a/relocation_planner/admin.py +++ b/relocation_planner/admin.py @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/relocation_planner/forms/__init__.py b/relocation_planner/forms/__init__.py new file mode 100644 index 0000000..2525587 --- /dev/null +++ b/relocation_planner/forms/__init__.py @@ -0,0 +1,13 @@ +from .edit_country import EditCountryForm +from .edit_pet_relocation_data import EditPetRelocationRequirementForm +from .edit_visa_requirement import EditVisaRequirementForm +from .edit_visa import EditVisaForm +from .visa_formset import EditVisaFormSet + +__all__ = [ + "EditCountryForm", + "EditPetRelocationRequirementForm", + "EditVisaRequirementForm", + "EditVisaForm", + "EditVisaFormSet", +] diff --git a/relocation_planner/forms/edit_country.py b/relocation_planner/forms/edit_country.py new file mode 100644 index 0000000..2ff77e2 --- /dev/null +++ b/relocation_planner/forms/edit_country.py @@ -0,0 +1,21 @@ +from django import forms +from relocation_planner.models import Country, Language + +class EditCountryForm(forms.ModelForm): + common_languages = forms.ModelMultipleChoiceField( + queryset=Language.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) + + class Meta: + model = Country + fields = [ + "name", + "business_language", + "common_languages", + "cost_of_living_index", + "quality_of_life_index", + "has_universal_healthcare", + "pet_relocation_info_link", + ] diff --git a/relocation_planner/forms/edit_pet_relocation_data.py b/relocation_planner/forms/edit_pet_relocation_data.py new file mode 100644 index 0000000..cf4f82a --- /dev/null +++ b/relocation_planner/forms/edit_pet_relocation_data.py @@ -0,0 +1,7 @@ +from django import forms +from relocation_planner.models import PetRelocationRequirement + +class EditPetRelocationRequirementForm(forms.ModelForm): + class Meta: + model = PetRelocationRequirement + fields = ["animal", "type", "name", "description", "duration"] diff --git a/relocation_planner/forms/edit_visa.py b/relocation_planner/forms/edit_visa.py new file mode 100644 index 0000000..73ad7d2 --- /dev/null +++ b/relocation_planner/forms/edit_visa.py @@ -0,0 +1,9 @@ +from django import forms +from relocation_planner.models import Visa + +class EditVisaForm(forms.ModelForm): + DELETE = forms.BooleanField(required=False, widget=forms.HiddenInput()) + + class Meta: + model = Visa + fields = ["name", "duration", "description", "information_link"] diff --git a/relocation_planner/forms/edit_visa_requirement.py b/relocation_planner/forms/edit_visa_requirement.py new file mode 100644 index 0000000..f755c26 --- /dev/null +++ b/relocation_planner/forms/edit_visa_requirement.py @@ -0,0 +1,7 @@ +from django import forms +from relocation_planner.models import VisaRequirement + +class EditVisaRequirementForm(forms.ModelForm): + class Meta: + model = VisaRequirement + fields = ["name", "description"] diff --git a/relocation_planner/forms/visa_formset.py b/relocation_planner/forms/visa_formset.py new file mode 100644 index 0000000..00e8b70 --- /dev/null +++ b/relocation_planner/forms/visa_formset.py @@ -0,0 +1,20 @@ +from django.forms import inlineformset_factory +from relocation_planner.models import Country, Visa, VisaRequirement +from .edit_visa import EditVisaForm +from .edit_visa_requirement import EditVisaRequirementForm + +VisaRequirementFormSet = inlineformset_factory( + Visa, + VisaRequirement, + form=EditVisaRequirementForm, + extra=0, + can_delete=True +) + +VisaFormSet = inlineformset_factory( + Country, + Visa, + form=EditVisaForm, + extra=0, + can_delete=True +) \ No newline at end of file diff --git a/relocation_planner/migrations/0002_remove_costofliving_country_and_more.py b/relocation_planner/migrations/0002_remove_costofliving_country_and_more.py new file mode 100644 index 0000000..b4fe687 --- /dev/null +++ b/relocation_planner/migrations/0002_remove_costofliving_country_and_more.py @@ -0,0 +1,361 @@ +# Generated by Django 5.1.3 on 2024-11-20 01:30 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('relocation_planner', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='costofliving', + name='country', + ), + migrations.RemoveField( + model_name='costofliving', + name='created_by', + ), + migrations.RemoveField( + model_name='costofliving', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='edithistory', + name='content_type', + ), + migrations.RemoveField( + model_name='edithistory', + name='created_by', + ), + migrations.RemoveField( + model_name='edithistory', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='healthcareinfo', + name='country', + ), + migrations.RemoveField( + model_name='healthcareinfo', + name='created_by', + ), + migrations.RemoveField( + model_name='healthcareinfo', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='propertycost', + name='country', + ), + migrations.RemoveField( + model_name='propertycost', + name='created_by', + ), + migrations.RemoveField( + model_name='propertycost', + name='last_modified_by', + ), + migrations.AlterUniqueTogether( + name='rating', + unique_together=None, + ), + migrations.RemoveField( + model_name='rating', + name='country', + ), + migrations.RemoveField( + model_name='rating', + name='created_by', + ), + migrations.RemoveField( + model_name='rating', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='rating', + name='user', + ), + migrations.AlterUniqueTogether( + name='relocationprocess', + unique_together=None, + ), + migrations.RemoveField( + model_name='relocationprocess', + name='country', + ), + migrations.RemoveField( + model_name='relocationprocess', + name='created_by', + ), + migrations.RemoveField( + model_name='relocationprocess', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='resourcelink', + name='country', + ), + migrations.RemoveField( + model_name='resourcelink', + name='created_by', + ), + migrations.RemoveField( + model_name='resourcelink', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='userexperience', + name='country', + ), + migrations.RemoveField( + model_name='userexperience', + name='created_by', + ), + migrations.RemoveField( + model_name='userexperience', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='userexperience', + name='user', + ), + migrations.AlterUniqueTogether( + name='visatype', + unique_together=None, + ), + migrations.RemoveField( + model_name='visatype', + name='country', + ), + migrations.RemoveField( + model_name='visatype', + name='created_by', + ), + migrations.RemoveField( + model_name='visatype', + name='last_modified_by', + ), + migrations.RemoveField( + model_name='visarequirement', + name='visa_type', + ), + migrations.RemoveField( + model_name='country', + name='avg_cost_of_living', + ), + migrations.RemoveField( + model_name='country', + name='avg_house_cost', + ), + migrations.RemoveField( + model_name='country', + name='avg_rent_cost', + ), + migrations.RemoveField( + model_name='country', + name='capital_city', + ), + migrations.RemoveField( + model_name='country', + name='currency', + ), + migrations.RemoveField( + model_name='country', + name='official_languages', + ), + migrations.RemoveField( + model_name='country', + name='population', + ), + migrations.RemoveField( + model_name='country', + name='timezone', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='country', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='documentation_required', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='estimated_cost', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='pet_type', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='quarantine_duration', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='quarantine_required', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='restrictions', + ), + migrations.RemoveField( + model_name='petrelocationrequirement', + name='vaccination_requirements', + ), + migrations.RemoveField( + model_name='visarequirement', + name='details', + ), + migrations.RemoveField( + model_name='visarequirement', + name='is_mandatory', + ), + migrations.RemoveField( + model_name='visarequirement', + name='requirement', + ), + migrations.AddField( + model_name='country', + name='common_languages', + field=models.ManyToManyField(related_name='countries', to='relocation_planner.language'), + ), + migrations.AddField( + model_name='country', + name='cost_of_living_index', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True), + ), + migrations.AddField( + model_name='country', + name='has_universal_healthcare', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='country', + name='pet_relocation_info_link', + field=models.URLField(blank=True), + ), + migrations.AddField( + model_name='country', + name='pet_relocation_requirements', + field=models.ManyToManyField(related_name='countries', to='relocation_planner.petrelocationrequirement'), + ), + migrations.AddField( + model_name='country', + name='quality_of_life_index', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True), + ), + migrations.AddField( + model_name='petrelocationrequirement', + name='description', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='petrelocationrequirement', + name='duration', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='petrelocationrequirement', + name='name', + field=models.CharField(default='name', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='petrelocationrequirement', + name='type', + field=models.CharField(choices=[('VACCINATION', 'Vaccination'), ('QUARANTINE', 'Quarantine'), ('DOCUMENTATION', 'Documentation'), ('FEE', 'Fee')], default='vaccine', max_length=20), + preserve_default=False, + ), + migrations.AddField( + model_name='visarequirement', + name='description', + field=models.TextField(default='justdefault'), + preserve_default=False, + ), + migrations.AddField( + model_name='visarequirement', + name='name', + field=models.CharField(default='default', max_length=100), + preserve_default=False, + ), + migrations.CreateModel( + name='AnimalSpecies', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=50, unique=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'animal species', + }, + ), + migrations.AddField( + model_name='petrelocationrequirement', + name='animal', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='relocation_planner.animalspecies'), + preserve_default=False, + ), + migrations.CreateModel( + name='Visa', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100)), + ('description', models.TextField()), + ('duration', models.CharField(max_length=50)), + ('information_link', models.URLField()), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('last_modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ('requirements', models.ManyToManyField(to='relocation_planner.visarequirement')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='country', + name='visas', + field=models.ManyToManyField(related_name='countries', to='relocation_planner.visa'), + ), + migrations.DeleteModel( + name='CitizenshipProcess', + ), + migrations.DeleteModel( + name='CostOfLiving', + ), + migrations.DeleteModel( + name='EditHistory', + ), + migrations.DeleteModel( + name='HealthcareInfo', + ), + migrations.DeleteModel( + name='PropertyCost', + ), + migrations.DeleteModel( + name='Rating', + ), + migrations.DeleteModel( + name='RelocationProcess', + ), + migrations.DeleteModel( + name='ResourceLink', + ), + migrations.DeleteModel( + name='UserExperience', + ), + migrations.DeleteModel( + name='VisaType', + ), + ] diff --git a/relocation_planner/migrations/0003_remove_country_visas_remove_visa_requirements_and_more.py b/relocation_planner/migrations/0003_remove_country_visas_remove_visa_requirements_and_more.py new file mode 100644 index 0000000..fac01c9 --- /dev/null +++ b/relocation_planner/migrations/0003_remove_country_visas_remove_visa_requirements_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.3 on 2024-11-21 18:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('relocation_planner', '0002_remove_costofliving_country_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='country', + name='visas', + ), + migrations.RemoveField( + model_name='visa', + name='requirements', + ), + migrations.AddField( + model_name='visa', + name='country', + field=models.ForeignKey(default=4, on_delete=django.db.models.deletion.CASCADE, related_name='visa_list', to='relocation_planner.country'), + preserve_default=False, + ), + migrations.AddField( + model_name='visarequirement', + name='visa', + field=models.ForeignKey(default=5, on_delete=django.db.models.deletion.CASCADE, related_name='requirement_list', to='relocation_planner.visa'), + preserve_default=False, + ), + ] diff --git a/relocation_planner/models.py b/relocation_planner/models.py index 526b0a3..c0261aa 100644 --- a/relocation_planner/models.py +++ b/relocation_planner/models.py @@ -1,6 +1,4 @@ from django.db import models -from django.core.validators import MinValueValidator, MaxValueValidator -from django.contrib.auth.models import User from django.utils.text import slugify from django.conf import settings @@ -24,237 +22,104 @@ class BaseModel(models.Model): class Meta: abstract = True -class Country(BaseModel): - """Core country information""" - name = models.CharField(max_length=100, unique=True) - slug = models.SlugField(max_length=100, unique=True) - official_languages = models.ManyToManyField('Language', related_name='countries') - business_language = models.ForeignKey( - 'Language', - on_delete=models.SET_NULL, - null=True, - related_name='business_countries' - ) - currency = models.CharField(max_length=50) - capital_city = models.CharField(max_length=100) - population = models.PositiveIntegerField() - timezone = models.CharField(max_length=50) - - # Automatically updated statistics - avg_cost_of_living = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True - ) - avg_house_cost = models.DecimalField( - max_digits=12, - decimal_places=2, - null=True, - blank=True - ) - avg_rent_cost = models.DecimalField( - max_digits=10, - decimal_places=2, - null=True, - blank=True - ) - - def save(self, *args, **kwargs): - if not self.slug: - self.slug = slugify(self.name) - super().save(*args, **kwargs) - - class Meta: - verbose_name_plural = "countries" - class Language(BaseModel): """Language information""" name = models.CharField(max_length=50, unique=True) code = models.CharField(max_length=10, unique=True) # ISO code -class Rating(BaseModel): - """User ratings for various country aspects""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - category = models.CharField(max_length=50, choices=[ - ('SAFETY', 'Safety'), - ('HEALTHCARE', 'Healthcare Quality'), - ('EDUCATION', 'Education Quality'), - ('HAPPINESS', 'Overall Happiness'), - ('INFRASTRUCTURE', 'Infrastructure'), - ('FOREIGNER_FRIENDLY', 'Foreigner Friendliness'), - ('WORK_LIFE', 'Work-Life Balance'), - ('ENVIRONMENT', 'Environmental Quality') - ]) - score = models.IntegerField( - validators=[MinValueValidator(1), MaxValueValidator(5)] - ) - comment = models.TextField(blank=True) + def __str__(self): + return f"{self.name} ({self.code})" + +class AnimalSpecies(BaseModel): + """Animal species that can be relocated""" + name = models.CharField(max_length=50, unique=True) + + def __str__(self): + return self.name class Meta: - unique_together = ['country', 'user', 'category'] + verbose_name_plural = "animal species" + +class VisaRequirement(BaseModel): + visa = models.ForeignKey( + 'Visa', + on_delete=models.CASCADE, + related_name='requirement_list' + ) + name = models.CharField(max_length=100) + description = models.TextField() + + def __str__(self): + return self.name -class VisaType(BaseModel): - """Different types of visas available""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) +class Visa(BaseModel): + country = models.ForeignKey( + 'Country', + on_delete=models.CASCADE, + related_name='visa_list' + ) name = models.CharField(max_length=100) description = models.TextField() duration = models.CharField(max_length=50) - processing_time = models.CharField(max_length=50) - allows_family = models.BooleanField() - family_notes = models.TextField(blank=True) - can_work = models.BooleanField() - path_to_citizenship = models.BooleanField() - - class Meta: - unique_together = ['country', 'name'] + information_link = models.URLField() -class VisaRequirement(BaseModel): - """Requirements for specific visa types""" - visa_type = models.ForeignKey(VisaType, on_delete=models.CASCADE) - requirement = models.CharField(max_length=255) - details = models.TextField() - is_mandatory = models.BooleanField(default=True) + def __str__(self): + return self.name -class CostOfLiving(BaseModel): - """User-submitted cost of living data""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - city = models.CharField(max_length=100) - date_recorded = models.DateField() - - # Monthly costs - rent_1bed_city = models.DecimalField(max_digits=10, decimal_places=2) - rent_3bed_city = models.DecimalField(max_digits=10, decimal_places=2) - rent_1bed_suburban = models.DecimalField(max_digits=10, decimal_places=2) - rent_3bed_suburban = models.DecimalField(max_digits=10, decimal_places=2) - utilities = models.DecimalField(max_digits=8, decimal_places=2) - internet = models.DecimalField(max_digits=8, decimal_places=2) - groceries = models.DecimalField(max_digits=8, decimal_places=2) - transportation = models.DecimalField(max_digits=8, decimal_places=2) - - class Meta: - verbose_name_plural = "costs of living" +class PetRelocationRequirement(BaseModel): + """Requirements for relocating with pets""" + animal = models.ForeignKey(AnimalSpecies, on_delete=models.CASCADE) + TYPE_CHOICES = [ + ('VACCINATION', 'Vaccination'), + ('QUARANTINE', 'Quarantine'), + ('DOCUMENTATION', 'Documentation'), + ('FEE', 'Fee'), + ] + type = models.CharField(max_length=20, choices=TYPE_CHOICES) + name = models.CharField(max_length=100) + description = models.TextField(blank=True) + duration = models.CharField(max_length=50, blank=True) -class PropertyCost(BaseModel): - """User-submitted property cost data""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - city = models.CharField(max_length=100) - date_recorded = models.DateField() - property_type = models.CharField(max_length=50, choices=[ - ('APARTMENT', 'Apartment'), - ('HOUSE', 'House'), - ('LAND', 'Land') - ]) - size_sqm = models.DecimalField(max_digits=8, decimal_places=2) - price = models.DecimalField(max_digits=12, decimal_places=2) - location_type = models.CharField(max_length=20, choices=[ - ('CITY_CENTER', 'City Center'), - ('SUBURBAN', 'Suburban'), - ('RURAL', 'Rural') - ]) + def __str__(self): + return f"{self.animal.name} - {self.get_type_display()} - {self.name}" -class RelocationProcess(BaseModel): - """Documentation of relocation processes""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - step_number = models.PositiveIntegerField() - title = models.CharField(max_length=200) - description = models.TextField() - estimated_time = models.CharField(max_length=50) - estimated_cost = models.DecimalField( - max_digits=10, - decimal_places=2, +class Country(BaseModel): + """Core country information""" + name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=100, unique=True) + common_languages = models.ManyToManyField(Language, related_name='countries') + business_language = models.ForeignKey( + Language, + on_delete=models.SET_NULL, null=True, - blank=True + related_name='business_countries' ) - required_documents = models.TextField() - - class Meta: - unique_together = ['country', 'step_number'] - ordering = ['step_number'] - -class PetRelocationRequirement(BaseModel): - """Requirements for relocating with pets""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - pet_type = models.CharField(max_length=50) # dog, cat, bird, etc. - quarantine_required = models.BooleanField() - quarantine_duration = models.CharField(max_length=50, blank=True) - vaccination_requirements = models.TextField() - documentation_required = models.TextField() - restrictions = models.TextField(blank=True) - estimated_cost = models.DecimalField( - max_digits=8, + cost_of_living_index = models.DecimalField( + max_digits=5, decimal_places=2, null=True, blank=True ) - -class CitizenshipProcess(BaseModel): - """Documentation of naturalization processes""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - residence_requirement_years = models.PositiveIntegerField() - language_requirement = models.TextField() - dual_citizenship_allowed = models.BooleanField() - test_required = models.BooleanField() - test_details = models.TextField(blank=True) - cost = models.DecimalField(max_digits=10, decimal_places=2) - process_details = models.TextField() - -class HealthcareInfo(BaseModel): - """Healthcare system information""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - has_universal_healthcare = models.BooleanField() - immigrant_coverage_waiting_period = models.CharField(max_length=100) - private_insurance_requirement = models.TextField() - avg_cost_private_insurance = models.DecimalField( - max_digits=8, + quality_of_life_index = models.DecimalField( + max_digits=5, decimal_places=2, null=True, blank=True ) - healthcare_system_details = models.TextField() - -class ResourceLink(BaseModel): - """External resources and references""" - country = models.ForeignKey(Country, on_delete=models.CASCADE) - title = models.CharField(max_length=200) - url = models.URLField() - category = models.CharField(max_length=50, choices=[ - ('OFFICIAL', 'Official Government Resource'), - ('HEALTHCARE', 'Healthcare Information'), - ('HOUSING', 'Housing Information'), - ('EDUCATION', 'Education Information'), - ('VISA', 'Visa Information'), - ('EMPLOYMENT', 'Employment Information'), - ('COMMUNITY', 'Community Resource') - ]) - description = models.TextField() - is_verified = models.BooleanField(default=False) + has_universal_healthcare = models.BooleanField(default=False) + pet_relocation_requirements = models.ManyToManyField( + PetRelocationRequirement, + related_name='countries' + ) + pet_relocation_info_link = models.URLField(blank=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) -class UserExperience(BaseModel): - """User experiences and stories""" - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - country = models.ForeignKey(Country, on_delete=models.CASCADE) - title = models.CharField(max_length=200) - content = models.TextField() - moved_from = models.CharField(max_length=100) - year_moved = models.PositiveIntegerField() - current_status = models.CharField(max_length=50, choices=[ - ('PLANNING', 'Planning Stage'), - ('IN_PROGRESS', 'Currently Relocating'), - ('RELOCATED', 'Successfully Relocated'), - ('RETURNED', 'Returned to Origin') - ]) - would_recommend = models.BooleanField() - challenges_faced = models.TextField() - tips = models.TextField() + def __str__(self): + return self.name -class EditHistory(BaseModel): - """Track changes to content""" - content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - field_name = models.CharField(max_length=100) - old_value = models.TextField(blank=True) - new_value = models.TextField(blank=True) - change_reason = models.TextField() - \ No newline at end of file + class Meta: + verbose_name_plural = "countries" \ No newline at end of file diff --git a/relocation_planner/templates/relocation_planner/assessment.html b/relocation_planner/templates/relocation_planner/assessment.html new file mode 100644 index 0000000..eff83d8 --- /dev/null +++ b/relocation_planner/templates/relocation_planner/assessment.html @@ -0,0 +1,24 @@ +{% extends "users/base.html" %} + +{% block title %}Relocation Planner - Assessment{% endblock %} + +{% block content %} +
+

Relocation Assessment

+

Answer a few questions to get personalized recommendations.

+ +
+ {% csrf_token %} +
+ + +
+
+ +
+
+
+{% endblock %} diff --git a/relocation_planner/templates/relocation_planner/assessment_results.html b/relocation_planner/templates/relocation_planner/assessment_results.html new file mode 100644 index 0000000..be0c9c4 --- /dev/null +++ b/relocation_planner/templates/relocation_planner/assessment_results.html @@ -0,0 +1,24 @@ +{% extends "users/base.html" %} + +{% block title %}Relocation Planner - Assessment Results{% endblock %} + +{% block content %} +
+

Assessment Results

+

Based on your answers, here are the top recommendations:

+ +
+ {% for result in results %} +
+

{{ result.name }}

+

{{ result.description }}

+ + View Details + +
+ {% empty %} +

No results found. Try adjusting your priorities or inputs.

+ {% endfor %} +
+
+{% endblock %} diff --git a/relocation_planner/templates/relocation_planner/browse.html b/relocation_planner/templates/relocation_planner/browse.html new file mode 100644 index 0000000..a844a22 --- /dev/null +++ b/relocation_planner/templates/relocation_planner/browse.html @@ -0,0 +1,34 @@ +{% extends "users/base.html" %} +{% load humanize %} + +{% block title %}Relocation Planner - Browse Countries{% endblock %} + +{% block content %} +
+
+

Browse Countries

+

Discover detailed insights and user-submitted information for each country.

+
+ + + +
+ {% for country in countries %} +
+

{{ country.name }}

+

Capital: {{ country.capital_city }}

+

Population: {{ country.population|intcomma }}

+ + View Details + +
+ {% empty %} +

No countries found. Add a new one using the button above!

+ {% endfor %} +
+
+{% endblock %} diff --git a/relocation_planner/templates/relocation_planner/country_detail.html b/relocation_planner/templates/relocation_planner/country_detail.html new file mode 100644 index 0000000..6eb2b73 --- /dev/null +++ b/relocation_planner/templates/relocation_planner/country_detail.html @@ -0,0 +1,184 @@ +{% extends "users/base.html" %} +{% load humanize %} + +{% block title %}{{ country.name }} - Relocation Planner{% endblock %} + +{% block content %} +
+ {# Header Section with Edit Button #} +
+

{{ country.name }}

+ {% if user.is_authenticated %} + + Edit Country + + {% endif %} +
+ + {# Language Information Card #} +
+

Language Information

+
+
+

Business Language

+

{{ country.business_language.name|default:"Not specified" }}

+
+
+

Common Languages

+

+ {% for language in country.common_languages.all %} + {{ language.name }}{% if not forloop.last %}, {% endif %} + {% empty %} + None specified + {% endfor %} +

+
+
+
+ + {# Quality Metrics Card #} +
+

Quality of Life

+
+
+

Cost of Living Index

+

+ {% if country.cost_of_living_index %} + {{ country.cost_of_living_index|floatformat:2 }} + {% else %} + Not available + {% endif %} +

+
+
+

Quality of Life Index

+

+ {% if country.quality_of_life_index %} + {{ country.quality_of_life_index|floatformat:2 }} + {% else %} + Not available + {% endif %} +

+
+
+

Healthcare System

+

+ {% if country.has_universal_healthcare %} + + Universal Healthcare Available + + {% else %} + + No Universal Healthcare + + {% endif %} +

+
+
+
+ + {# Visa Information Card #} +
+

Visa Options

+ {% if country.visa_list.exists %} +
+ {% for visa in country.visa_list.all %} +
+

{{ visa.name }}

+
+
+
Duration
+
{{ visa.duration }}
+
+
+
Requirements
+
+
    + {% for req in visa.requirement_list.all %} +
  • {{ req.name }}
  • + {% endfor %} +
+
+
+
+ {% if visa.information_link %} + + {% endif %} +
+ {% endfor %} +
+ {% else %} +

No visa information available.

+ {% endif %} +
+ + {# Pet Relocation Card #} +
+

Pet Relocation Requirements

+ {% if country.pet_relocation_requirements.exists %} + {% regroup country.pet_relocation_requirements.all by animal as animal_list %} +
+ {% for animal in animal_list %} +
+

{{ animal.grouper.name }}

+
+ {% for req in animal.list %} +
+

+ {{ req.get_type_display }} +

+

{{ req.name }}

+ {% if req.description %} +

{{ req.description }}

+ {% endif %} + {% if req.duration %} +

Duration: {{ req.duration }}

+ {% endif %} +
+ {% endfor %} +
+
+ {% endfor %} +
+ {% if country.pet_relocation_info_link %} + + {% endif %} + {% else %} +

No pet relocation information available.

+ {% if country.pet_relocation_info_link %} + + {% endif %} + {% endif %} +
+ + {# Last Updated Information #} +
+

Last updated: {{ country.updated_at|date:"F j, Y" }}

+ {% if country.last_modified_by %} +

Updated by: {{ country.last_modified_by.username }}

+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/relocation_planner/templates/relocation_planner/country_list.html b/relocation_planner/templates/relocation_planner/country_list.html deleted file mode 100644 index f14596d..0000000 --- a/relocation_planner/templates/relocation_planner/country_list.html +++ /dev/null @@ -1,87 +0,0 @@ -{# relocation_planner/templates/relocation_planner/country_list.html #} -{% extends 'relocation_planner/base.html' %} - -{% block relocation_content %} -
- {# Search and Filter Section #} -
-
-
- - -
- - {% if request.GET.search %} - - Clear - - {% endif %} -
-
- - {# Countries Grid #} -
- {% for country in countries %} -
-

- - {{ country.name }} - -

- -
-

Capital: {{ country.capital_city }}

-

Population: {{ country.population|intcomma }}

- - {% if country.official_languages.exists %} -

Languages: - {{ country.official_languages.all|join:", " }} -

- {% endif %} - - {% if country.avg_cost_of_living %} -

Avg. Cost of Living: ${{ country.avg_cost_of_living|floatformat:2 }}

- {% endif %} -
- - -
- {% empty %} -
- No countries found. - {% if user.is_authenticated %} - - Add one? - - {% endif %} -
- {% endfor %} -
- - {# Add Country Button #} - {% if user.is_authenticated %} - - {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/relocation_planner/templates/relocation_planner/edit_country.html b/relocation_planner/templates/relocation_planner/edit_country.html new file mode 100644 index 0000000..b541c73 --- /dev/null +++ b/relocation_planner/templates/relocation_planner/edit_country.html @@ -0,0 +1,248 @@ +{% extends "users/base.html" %} +{% load dict_filters %} + +{% block content %} +
+

+ {% if country %}Edit {{ country.name }}{% else %}Add New Country{% endif %} +

+ +
+ {% csrf_token %} + + {# Basic Info #} +
+

Basic Information

+
+ {% for field in country_form %} +
+ + {{ field }} + {% if field.errors %} +

{{ field.errors|join:", " }}

+ {% endif %} +
+ {% endfor %} +
+
+ + {# Visas #} +
+

Visa Information

+ {{ visa_formset.management_form }} + +
+ {% for visa_form in visa_formset %} +
+

Visa {{ forloop.counter }}

+ +
+ {% for field in visa_form.visible_fields %} +
+ + {{ field }} +
+ {% endfor %} +
+ + {# Requirements #} +
+

Requirements

+ {% with visa_id=visa_form.instance.id|default:forloop.counter0 %} + {% with req_formset=requirement_formsets|get_item:visa_id %} + {% if req_formset %} + {{ req_formset.management_form }} +
+ {% for req_form in req_formset %} +
+
+
+ + {{ req_form.name }} + {{ req_form.id }} +
+
+ + {{ req_form.description }} +
+
+ {{ req_form.DELETE }} + +
+ {% endfor %} +
+ {% endif %} + + {% endwith %} + {% endwith %} +
+ + {{ visa_form.DELETE }} + +
+ {% endfor %} +
+ + +
+ +
+ + Cancel + + +
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/relocation_planner/templates/relocation_planner/landing.html b/relocation_planner/templates/relocation_planner/landing.html index 1d64bcd..ace205f 100644 --- a/relocation_planner/templates/relocation_planner/landing.html +++ b/relocation_planner/templates/relocation_planner/landing.html @@ -1,43 +1,30 @@ -{% extends 'Users/base.html' %} +{% extends "users/base.html" %} -{% block title %}Relocation Planner - {{ block.super }}{% endblock %} +{% block title %}Relocation Planner - Home{% endblock %} {% block content %} -{# Sub-navigation #} - + -
- {# Hero Section #} -
-

Find Your New Home

-

Explore countries, compare options, and plan your international relocation

- - {% if not user.is_authenticated %} - - Sign in to contribute +
- - {# Rest of the template content remains the same #} - ...
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/relocation_planner/templatetags/__init__.py b/relocation_planner/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/relocation_planner/templatetags/dict_filters.py b/relocation_planner/templatetags/dict_filters.py new file mode 100644 index 0000000..420e579 --- /dev/null +++ b/relocation_planner/templatetags/dict_filters.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter +def get_item(dictionary, key): + return dictionary.get(key) \ No newline at end of file diff --git a/relocation_planner/urls.py b/relocation_planner/urls.py index f9a1219..0346b07 100644 --- a/relocation_planner/urls.py +++ b/relocation_planner/urls.py @@ -1,9 +1,22 @@ from django.urls import path -from . import views +from .views import ( + LandingView, + BrowseView, + CountryDetailView, + EditCountryView, + AssessmentView, + AssessmentResultsView +) app_name = 'relocation_planner' urlpatterns = [ - path('', views.LandingView.as_view(), name='landing'), - path('countries/', views.CountryListView.as_view(), name='country-list'), + path('', LandingView.as_view(), name='landing'), + path('browse/', BrowseView.as_view(), name='browse'), + path('assess/', AssessmentView.as_view(), name='assessment'), + path('assess/results/', AssessmentResultsView.as_view(), name='assessment_results'), + # Make sure the add path comes before the detail path to avoid slug conflicts + path('country/add/', EditCountryView.as_view(), name='add_country'), + path('country//edit/', EditCountryView.as_view(), name='edit_country'), + path('country//', CountryDetailView.as_view(), name='country_detail'), ] \ No newline at end of file diff --git a/relocation_planner/views.py b/relocation_planner/views.py index 0c1f4b4..b15aa07 100644 --- a/relocation_planner/views.py +++ b/relocation_planner/views.py @@ -1,55 +1,333 @@ -from django.views.generic import ListView, TemplateView -from django.db.models import Avg, Count -from .models import Country, UserExperience, Rating - -class LandingView(TemplateView): - template_name = 'relocation_planner/landing.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - # Get featured countries - for now, selecting countries with the most data - # We could modify this logic based on your preferences - featured_countries = Country.objects.annotate( - rating_count=Count('rating'), - experience_count=Count('userexperience'), - data_richness=Count('rating') + Count('userexperience') - ).order_by('-data_richness')[:6] # Adjust number as needed - - # Get recent experiences - recent_experiences = UserExperience.objects.select_related( - 'user', 'country' - ).order_by('-created_at')[:5] - - context.update({ - 'featured_countries': featured_countries, - 'recent_experiences': recent_experiences, +from django.contrib import messages +from django.db import transaction +from django.forms import modelformset_factory +from django.shortcuts import get_object_or_404, redirect, render +from django.views import View +from .forms.edit_country import EditCountryForm +from .forms.visa_formset import VisaFormSet, VisaRequirementFormSet +from .models import ( + Country, + Language, + Visa, + VisaRequirement, + PetRelocationRequirement, + AnimalSpecies +) + +import logging +logger = logging.getLogger(__name__) + +class LandingView(View): + def get(self, request): + return render(request, "relocation_planner/landing.html") + +class BrowseView(View): + def get(self, request): + countries = Country.objects.all().order_by('name') + return render(request, "relocation_planner/browse.html", { + "countries": countries + }) + +class CountryDetailView(View): + def get(self, request, slug): + country = get_object_or_404( + Country.objects.prefetch_related( + 'common_languages', + 'visa_list', + 'visa_list__requirement_list', + 'pet_relocation_requirements', + 'pet_relocation_requirements__animal' + ).select_related('business_language'), + slug=slug + ) + return render(request, "relocation_planner/country_detail.html", { + "country": country }) - return context - -class CountryListView(ListView): - model = Country - template_name = 'relocation_planner/country_list.html' - context_object_name = 'countries' - paginate_by = 12 # Shows 12 countries per page - - def get_queryset(self): - queryset = Country.objects.annotate( - avg_rating=Avg('rating__score') - ).select_related( - 'business_language' - ).prefetch_related( - 'official_languages' + +# class EditCountryView(View): +# def get(self, request, slug=None): +# logger.debug(f"GET request received for slug: {slug}") +# try: +# country = None +# if slug: +# country = get_object_or_404(Country, slug=slug) + +# context = { +# "country": country, +# "languages": Language.objects.all().order_by('name'), +# "visas": Visa.objects.all().order_by('name'), +# "pet_requirements": PetRelocationRequirement.objects.all().order_by('animal__name', 'type'), +# "animals": AnimalSpecies.objects.all().order_by('name'), +# } + +# return render(request, "relocation_planner/edit_country.html", context) +# except Exception as e: +# logger.error(f"Error in GET: {str(e)}") +# raise + +# def post(self, request, slug=None): +# logger.debug("POST data received:") +# for key, value in request.POST.items(): +# logger.debug(f"{key}: {value}") + +# try: +# # Start a transaction since we're handling multiple related models +# with transaction.atomic(): +# if slug: +# country = get_object_or_404(Country, slug=slug) +# else: +# country = Country() + +# # Update basic country fields as before... +# country.name = request.POST.get("name") +# country.cost_of_living_index = request.POST.get("cost_of_living_index") or None +# country.quality_of_life_index = request.POST.get("quality_of_life_index") or None +# country.has_universal_healthcare = request.POST.get("has_universal_healthcare") == "on" +# country.pet_relocation_info_link = request.POST.get("pet_relocation_info_link", "") +# country.business_language_id = request.POST.get("business_language") or None + +# # Set the user who made the changes +# if not country.id: +# country.created_by = request.user +# country.last_modified_by = request.user + +# country.save() + +# # Update many-to-many relationships +# common_languages_ids = request.POST.getlist("common_languages") +# country.common_languages.set(common_languages_ids) + +# # Handle existing visas +# visa_ids = request.POST.getlist("visa_ids[]") +# existing_visas = set() # Keep track of visas we're updating + +# for visa_id in visa_ids: +# logger.debug(f"Processing visa {visa_id}") + +# try: +# visa = Visa.objects.get(id=visa_id) +# existing_visas.add(visa.id) + +# # Update visa fields +# visa.name = request.POST.get(f"visa_name_{visa_id}") +# visa.duration = request.POST.get(f"visa_duration_{visa_id}") +# visa.description = request.POST.get(f"visa_description_{visa_id}") +# visa.information_link = request.POST.get(f"visa_information_link_{visa_id}") +# visa.last_modified_by = request.user +# visa.save() + +# # Handle existing requirements +# req_ids = request.POST.getlist(f"visa_requirement_ids_{visa_id}[]", []) +# logger.debug(f"Found existing requirement IDs: {req_ids}") +# existing_reqs = set() + +# for req_id in req_ids: +# try: +# req = VisaRequirement.objects.get(id=req_id) +# existing_reqs.add(req.id) + +# req.name = request.POST.get(f"visa_requirement_name_{visa_id}_{req_id}") +# req.description = request.POST.get(f"visa_requirement_description_{visa_id}_{req_id}") +# req.last_modified_by = request.user +# req.save() +# except VisaRequirement.DoesNotExist: +# logger.warning(f"Requirement {req_id} not found for visa {visa_id}") + +# # Handle new requirements for existing visa +# # These come from the "Add Requirement" button in the form +# requirement_names = request.POST.getlist(f"new_visa_requirement_name_{visa_id}[]", []) +# requirement_descriptions = request.POST.getlist(f"new_visa_requirement_description_{visa_id}[]", []) + +# logger.debug(f"New requirements for visa {visa_id}: Names={requirement_names}, Descriptions={requirement_descriptions}") + +# for name, description in zip(requirement_names, requirement_descriptions): +# if name.strip(): # Only create if name is provided and not just whitespace +# new_req = VisaRequirement.objects.create( +# visa=visa, +# name=name, +# description=description, +# created_by=request.user, +# last_modified_by=request.user +# ) +# existing_reqs.add(new_req.id) +# logger.debug(f"Created new requirement: {new_req.id} for visa {visa_id}") + +# # Clean up removed requirements +# visa.requirements.exclude(id__in=existing_reqs).delete() + +# except Visa.DoesNotExist: +# messages.warning(request, f"Visa with ID {visa_id} not found") + +# # Handle new visas and their requirements (this part seems to be working) +# new_visa_names = request.POST.getlist("new_visa_name[]") +# new_visa_durations = request.POST.getlist("new_visa_duration[]") +# new_visa_descriptions = request.POST.getlist("new_visa_description[]") +# new_visa_links = request.POST.getlist("new_visa_information_link[]") + +# for i in range(len(new_visa_names)): +# if new_visa_names[i]: # Only create if name is provided +# visa = Visa.objects.create( +# name=new_visa_names[i], +# duration=new_visa_durations[i], +# description=new_visa_descriptions[i], +# information_link=new_visa_links[i], +# created_by=request.user, +# last_modified_by=request.user +# ) +# country.visas.add(visa) +# existing_visas.add(visa.id) + +# # Handle requirements for the new visa +# new_req_names = request.POST.getlist(f"new_requirement_name_new_{i}[]") +# new_req_descriptions = request.POST.getlist(f"new_requirement_description_new_{i}[]") +# logger.debug(f"Found new requirements for visa {visa_id}:") +# logger.debug(f"Names: {new_req_names}") +# logger.debug(f"Descriptions: {new_req_descriptions}") + +# for name, desc in zip(new_req_names, new_req_descriptions): +# if name.strip(): # Only create if name is provided and not just whitespace +# VisaRequirement.objects.create( +# visa=visa, +# name=name, +# description=desc, +# created_by=request.user, +# last_modified_by=request.user +# ) + +# # Clean up visas that were removed +# country.visas.exclude(id__in=existing_visas).delete() + +# messages.success(request, f"Country {'updated' if slug else 'added'} successfully!") +# return redirect("relocation_planner:country_detail", slug=country.slug) + +# except Exception as e: +# logger.error(f"Error saving country: {str(e)}") +# messages.error(request, f"An error occurred: {str(e)}") +# return self.get(request, slug) + +class EditCountryView(View): + template_name = "relocation_planner/edit_country.html" + + def get_visa_requirement_formsets(self, visas): + requirement_formsets = {} + for visa in visas: + formset = VisaRequirementFormSet( + instance=visa, + prefix=f'requirements_{visa.id}' if visa.id else f'requirements_new_{len(requirement_formsets)}' + ) + requirement_formsets[visa.id if visa.id else f'new_{len(requirement_formsets)}'] = formset + return requirement_formsets + + def get(self, request, slug=None): + country = get_object_or_404(Country, slug=slug) if slug else None + country_form = EditCountryForm(instance=country) + visa_formset = VisaFormSet(instance=country if country else None, prefix='visas') + requirement_formsets = self.get_visa_requirement_formsets( + country.visa_list.all() if country else [] ) - - # Handle search - search_query = self.request.GET.get('search', '').strip() - if search_query: - queryset = queryset.filter(name__icontains=search_query) - - return queryset.order_by('name') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['search_query'] = self.request.GET.get('search', '') - return context \ No newline at end of file + + context = { + "country": country, + "country_form": country_form, + "visa_formset": visa_formset, + "requirement_formsets": requirement_formsets, + } + return render(request, self.template_name, context) + + def post(self, request, slug=None): + country = get_object_or_404(Country, slug=slug) if slug else None + country_form = EditCountryForm(request.POST, instance=country) + visa_formset = VisaFormSet(request.POST, instance=country) + + requirement_formsets = {} + is_valid = country_form.is_valid() and visa_formset.is_valid() + + # Process existing visas + if is_valid: + country = country_form.save() + visas = visa_formset.save(commit=False) + + # Handle visa deletions + for obj in visa_formset.deleted_objects: + obj.delete() + + # Process each visa and its requirements + for visa_form in visa_formset.forms: + if not visa_form.cleaned_data.get('DELETE', False): + visa = visa_form.save(commit=False) + visa.country = country + visa.save() + + # Process requirements for this visa + prefix = f'visa_{visa.id}_requirements' if visa.id else f'new_visa_{len(requirement_formsets)}_requirements' + requirement_formset = VisaRequirementFormSet( + request.POST, + instance=visa, + prefix=prefix + ) + + if requirement_formset.is_valid(): + requirements = requirement_formset.save(commit=False) + for req in requirement_formset.deleted_objects: + req.delete() + for requirement in requirements: + requirement.visa = visa + requirement.save() + else: + is_valid = False + requirement_formsets[visa.id if visa.id else f'new_{len(requirement_formsets)}'] = requirement_formset + + if is_valid: + return redirect("relocation_planner:country_detail", slug=country.slug) + + # If we get here, there was a validation error + if not requirement_formsets: + requirement_formsets = self.get_visa_requirement_formsets( + [form.instance for form in visa_formset.forms if form.instance.pk and not form.cleaned_data.get('DELETE', False)] + ) + + context = { + "country": country, + "country_form": country_form, + "visa_formset": visa_formset, + "requirement_formsets": requirement_formsets, + } + return render(request, self.template_name, context) + +class AssessmentView(View): + def get(self, request): + return render(request, "relocation_planner/assessment.html") + + def post(self, request): + try: + # Get user preferences + healthcare_required = request.POST.get('healthcare_required') == 'on' + cost_of_living_max = request.POST.get('cost_of_living_max') + quality_of_life_min = request.POST.get('quality_of_life_min') + + # Filter countries based on preferences + countries = Country.objects.all() + + if healthcare_required: + countries = countries.filter(has_universal_healthcare=True) + + if cost_of_living_max: + countries = countries.filter(cost_of_living_index__lte=cost_of_living_max) + + if quality_of_life_min: + countries = countries.filter(quality_of_life_index__gte=quality_of_life_min) + + return render(request, "relocation_planner/assessment_results.html", { + "countries": countries + }) + + except Exception as e: + logger.error(f"Error in assessment: {str(e)}") + messages.error(request, "An error occurred during assessment. Please try again.") + return redirect("relocation_planner:assessment") + +class AssessmentResultsView(View): + def get(self, request): + return render(request, "relocation_planner/assessment_results.html", { + "countries": [] + }) \ No newline at end of file From 046186619baed33afd4d544d7f87563ebcaab516 Mon Sep 17 00:00:00 2001 From: Brent Mills Date: Tue, 26 Nov 2024 20:51:16 -0500 Subject: [PATCH 3/8] Resolve missing hidden fields --- .../relocation_planner/edit_country.html | 59 +++++++++++---- relocation_planner/views.py | 74 +++++++++++++------ 2 files changed, 97 insertions(+), 36 deletions(-) diff --git a/relocation_planner/templates/relocation_planner/edit_country.html b/relocation_planner/templates/relocation_planner/edit_country.html index b541c73..d89cc2f 100644 --- a/relocation_planner/templates/relocation_planner/edit_country.html +++ b/relocation_planner/templates/relocation_planner/edit_country.html @@ -9,6 +9,17 @@

{% csrf_token %} + + {% if country_form.non_field_errors %} +
+ {{ country_form.non_field_errors }} +
+ {% endif %} + {% if visa_formset.non_form_errors %} +
+ {{ visa_formset.non_form_errors }} +
+ {% endif %} {# Basic Info #}
@@ -36,6 +47,10 @@

Visa Information

Visa {{ forloop.counter }}

+ {% for hidden in visa_form.hidden_fields %} + {{ hidden }} + {% endfor %} +
{% for field in visa_form.visible_fields %}
@@ -144,10 +159,25 @@

Requirements

const visaForms = document.getElementById('visa-forms'); const totalForms = document.querySelector('[name="visas-TOTAL_FORMS"]'); const newFormIdx = totalForms.value; - + const newUuid = crypto.randomUUID(); + + // Add management form for requirements + const reqManagementForm = ` + + + + + `; + const template = ` -
+

New Visa

+ + + {% for hidden in visa_formset.empty_form.hidden_fields %} + {{ hidden.as_widget|escapejs }} + {% endfor %} +
{% for field in visa_formset.empty_form.visible_fields %}
@@ -159,14 +189,14 @@

New Visa

Requirements

-
+
- +

`.replace(/__prefix__/g, newFormIdx); - + visaForms.insertAdjacentHTML('beforeend', template); totalForms.value = parseInt(totalForms.value) + 1; } function addRequirement(visaId) { const container = document.getElementById(`requirements-container-${visaId}`); - const reqFormPrefix = visaId.toString().startsWith('new_') ? + const reqFormPrefix = visaId.startsWith('new_') ? `new_visa_${visaId}_requirements` : `visa_${visaId}_requirements`; - + const totalFormsInput = document.querySelector(`[name="${reqFormPrefix}-TOTAL_FORMS"]`); const newFormIdx = totalFormsInput ? parseInt(totalFormsInput.value) : 0; - + const newUuid = crypto.randomUUID(); // Unique identifier for this form + const template = `
+ class="w-full border-gray-300 rounded-md">
+ class="w-full border-gray-300 rounded-md">
@@ -211,12 +242,14 @@

Requirements

`; - + container.insertAdjacentHTML('beforeend', template); + + // Increment TOTAL_FORMS count if (totalFormsInput) { totalFormsInput.value = newFormIdx + 1; } else { - // Create management form if it doesn't exist + // Add management form if it doesn't exist const managementForm = ` diff --git a/relocation_planner/views.py b/relocation_planner/views.py index b15aa07..dfae29e 100644 --- a/relocation_planner/views.py +++ b/relocation_planner/views.py @@ -14,6 +14,8 @@ AnimalSpecies ) +import uuid + import logging logger = logging.getLogger(__name__) @@ -205,17 +207,23 @@ def get(self, request, slug): # messages.error(request, f"An error occurred: {str(e)}") # return self.get(request, slug) +from django.shortcuts import render, redirect, get_object_or_404 +from django.views import View +import uuid + class EditCountryView(View): template_name = "relocation_planner/edit_country.html" def get_visa_requirement_formsets(self, visas): requirement_formsets = {} for visa in visas: + visa_id = visa.id if visa.id else f'new_{uuid.uuid4().hex}' + prefix = f'requirements_{visa_id}' formset = VisaRequirementFormSet( instance=visa, - prefix=f'requirements_{visa.id}' if visa.id else f'requirements_new_{len(requirement_formsets)}' + prefix=prefix ) - requirement_formsets[visa.id if visa.id else f'new_{len(requirement_formsets)}'] = formset + requirement_formsets[visa_id] = formset return requirement_formsets def get(self, request, slug=None): @@ -235,49 +243,69 @@ def get(self, request, slug=None): return render(request, self.template_name, context) def post(self, request, slug=None): + print("POST received") # Debug country = get_object_or_404(Country, slug=slug) if slug else None country_form = EditCountryForm(request.POST, instance=country) - visa_formset = VisaFormSet(request.POST, instance=country) + visa_formset = VisaFormSet(request.POST, instance=country if country else None, prefix='visas') + + print(f"Country form valid: {country_form.is_valid()}") # Debug + print(f"Visa formset valid: {visa_formset.is_valid()}") # Debug + if not visa_formset.is_valid(): + print(f"Visa formset errors: {visa_formset.errors}") # Debug requirement_formsets = {} is_valid = country_form.is_valid() and visa_formset.is_valid() - # Process existing visas + # Process existing visas and collect their requirement formsets if is_valid: - country = country_form.save() - visas = visa_formset.save(commit=False) - - # Handle visa deletions - for obj in visa_formset.deleted_objects: - obj.delete() - - # Process each visa and its requirements for visa_form in visa_formset.forms: if not visa_form.cleaned_data.get('DELETE', False): - visa = visa_form.save(commit=False) - visa.country = country - visa.save() - - # Process requirements for this visa - prefix = f'visa_{visa.id}_requirements' if visa.id else f'new_visa_{len(requirement_formsets)}_requirements' + visa = visa_form.instance + visa_id = visa.id if visa.id else f'new_{uuid.uuid4().hex}' + + # Get the correct prefix for requirements + prefix = f'requirements_{visa_id}' + + # Create requirement formset with the correct prefix requirement_formset = VisaRequirementFormSet( request.POST, instance=visa, prefix=prefix ) - + if requirement_formset.is_valid(): + requirement_formsets[visa_id] = requirement_formset + else: + is_valid = False + requirement_formsets[visa_id] = requirement_formset + + if is_valid: + country = country_form.save() + + # Save visas + for visa_form in visa_formset.forms: + if not visa_form.cleaned_data.get('DELETE', False): + visa = visa_form.save(commit=False) + visa.country = country + visa.save() + + # Get the corresponding requirement formset + visa_id = visa.id if visa.id else f'new_{uuid.uuid4().hex}' + requirement_formset = requirement_formsets.get(visa_id) + + if requirement_formset: + # Save requirements requirements = requirement_formset.save(commit=False) for req in requirement_formset.deleted_objects: req.delete() for requirement in requirements: requirement.visa = visa requirement.save() - else: - is_valid = False - requirement_formsets[visa.id if visa.id else f'new_{len(requirement_formsets)}'] = requirement_formset + else: + # Handle deletion + if visa_form.instance.pk: + visa_form.instance.delete() - if is_valid: return redirect("relocation_planner:country_detail", slug=country.slug) # If we get here, there was a validation error From 62082c018dd3dda30c88b0fe3193de401e1e986f Mon Sep 17 00:00:00 2001 From: Brent Mills Date: Tue, 26 Nov 2024 21:22:58 -0500 Subject: [PATCH 4/8] FINALLY get requirements working --- .../templates/relocation_planner/edit_country.html | 2 ++ relocation_planner/views.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/relocation_planner/templates/relocation_planner/edit_country.html b/relocation_planner/templates/relocation_planner/edit_country.html index d89cc2f..614ca9d 100644 --- a/relocation_planner/templates/relocation_planner/edit_country.html +++ b/relocation_planner/templates/relocation_planner/edit_country.html @@ -234,6 +234,8 @@

Requirements

class="w-full border-gray-300 rounded-md"> + + + +
+

+
+
+ +
+ + + +
{# Header Section with Edit Button #}
@@ -86,7 +114,10 @@

Visa Options

{% for visa in country.visa_list.all %}

{{ visa.name }}

-
+ {% if visa.description %} +

{{ visa.description }}

+ {% endif %} +
Duration
{{ visa.duration }}
@@ -94,9 +125,19 @@

{{ visa.name }}

Requirements
-
    +
      {% for req in visa.requirement_list.all %} -
    • {{ req.name }}
    • +
    • + +
    • {% endfor %}
@@ -105,9 +146,9 @@

{{ visa.name }}

{% if visa.information_link %} @@ -181,4 +222,34 @@

{% endif %}

+ + {% endblock %} \ No newline at end of file From 958b83684c8863b805ac16ef408687176c64a800 Mon Sep 17 00:00:00 2001 From: Brent Mills Date: Tue, 26 Nov 2024 23:24:52 -0500 Subject: [PATCH 6/8] Add pet relocation data to relocation planner --- relocation_planner/admin.py | 13 ++ relocation_planner/forms/__init__.py | 7 +- .../forms/edit_pet_relocation_data.py | 7 + .../pet_relocation_requirement_formset.py | 11 + ...ry_pet_relocation_requirements_and_more.py | 24 +++ relocation_planner/models.py | 9 +- .../relocation_planner/country_detail.html | 14 +- .../relocation_planner/edit_country.html | 116 +++++++++++ relocation_planner/views.py | 197 +++--------------- 9 files changed, 216 insertions(+), 182 deletions(-) create mode 100644 relocation_planner/forms/pet_relocation_requirement_formset.py create mode 100644 relocation_planner/migrations/0004_remove_country_pet_relocation_requirements_and_more.py diff --git a/relocation_planner/admin.py b/relocation_planner/admin.py index e69de29..acf1dc6 100644 --- a/relocation_planner/admin.py +++ b/relocation_planner/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from .models import AnimalSpecies, PetRelocationRequirement + +@admin.register(AnimalSpecies) +class AnimalSpeciesAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ('name',) + +@admin.register(PetRelocationRequirement) +class PetRelocationRequirementAdmin(admin.ModelAdmin): + list_display = ('name', 'animal', 'type', 'duration') + list_filter = ('animal', 'type') + search_fields = ('name', 'description') \ No newline at end of file diff --git a/relocation_planner/forms/__init__.py b/relocation_planner/forms/__init__.py index 2525587..3671efe 100644 --- a/relocation_planner/forms/__init__.py +++ b/relocation_planner/forms/__init__.py @@ -2,12 +2,15 @@ from .edit_pet_relocation_data import EditPetRelocationRequirementForm from .edit_visa_requirement import EditVisaRequirementForm from .edit_visa import EditVisaForm -from .visa_formset import EditVisaFormSet +from .pet_relocation_requirement_formset import PetRelocationRequirementFormSet +from .visa_formset import VisaFormSet, VisaRequirementFormSet __all__ = [ "EditCountryForm", "EditPetRelocationRequirementForm", "EditVisaRequirementForm", "EditVisaForm", - "EditVisaFormSet", + "PetRelocationRequirementFormSet", + "VisaFormSet", + "VisaRequirementFormSet", ] diff --git a/relocation_planner/forms/edit_pet_relocation_data.py b/relocation_planner/forms/edit_pet_relocation_data.py index cf4f82a..247de74 100644 --- a/relocation_planner/forms/edit_pet_relocation_data.py +++ b/relocation_planner/forms/edit_pet_relocation_data.py @@ -5,3 +5,10 @@ class EditPetRelocationRequirementForm(forms.ModelForm): class Meta: model = PetRelocationRequirement fields = ["animal", "type", "name", "description", "duration"] + widgets = { + 'type': forms.Select(attrs={'class': 'w-full border-gray-300 rounded-md'}), + 'animal': forms.Select(attrs={'class': 'w-full border-gray-300 rounded-md'}), + 'name': forms.TextInput(attrs={'class': 'w-full border-gray-300 rounded-md'}), + 'description': forms.Textarea(attrs={'class': 'w-full border-gray-300 rounded-md', 'rows': 3}), + 'duration': forms.TextInput(attrs={'class': 'w-full border-gray-300 rounded-md'}), + } \ No newline at end of file diff --git a/relocation_planner/forms/pet_relocation_requirement_formset.py b/relocation_planner/forms/pet_relocation_requirement_formset.py new file mode 100644 index 0000000..42492a9 --- /dev/null +++ b/relocation_planner/forms/pet_relocation_requirement_formset.py @@ -0,0 +1,11 @@ +from django.forms import inlineformset_factory +from relocation_planner.models import Country, PetRelocationRequirement +from relocation_planner.forms.edit_pet_relocation_data import EditPetRelocationRequirementForm + +PetRelocationRequirementFormSet = inlineformset_factory( + Country, + PetRelocationRequirement, + form=EditPetRelocationRequirementForm, + extra=0, + can_delete=True +) \ No newline at end of file diff --git a/relocation_planner/migrations/0004_remove_country_pet_relocation_requirements_and_more.py b/relocation_planner/migrations/0004_remove_country_pet_relocation_requirements_and_more.py new file mode 100644 index 0000000..7940f30 --- /dev/null +++ b/relocation_planner/migrations/0004_remove_country_pet_relocation_requirements_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.3 on 2024-11-27 03:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('relocation_planner', '0003_remove_country_visas_remove_visa_requirements_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='country', + name='pet_relocation_requirements', + ), + migrations.AddField( + model_name='petrelocationrequirement', + name='country', + field=models.ForeignKey(default='5', on_delete=django.db.models.deletion.CASCADE, related_name='pet_requirement_list', to='relocation_planner.country'), + preserve_default=False, + ), + ] diff --git a/relocation_planner/models.py b/relocation_planner/models.py index c0261aa..7c877c2 100644 --- a/relocation_planner/models.py +++ b/relocation_planner/models.py @@ -68,6 +68,11 @@ def __str__(self): class PetRelocationRequirement(BaseModel): """Requirements for relocating with pets""" + country = models.ForeignKey( + 'Country', + on_delete=models.CASCADE, + related_name='pet_requirement_list' # Changed from pet_relocation_requirements + ) animal = models.ForeignKey(AnimalSpecies, on_delete=models.CASCADE) TYPE_CHOICES = [ ('VACCINATION', 'Vaccination'), @@ -107,10 +112,6 @@ class Country(BaseModel): blank=True ) has_universal_healthcare = models.BooleanField(default=False) - pet_relocation_requirements = models.ManyToManyField( - PetRelocationRequirement, - related_name='countries' - ) pet_relocation_info_link = models.URLField(blank=True) def save(self, *args, **kwargs): diff --git a/relocation_planner/templates/relocation_planner/country_detail.html b/relocation_planner/templates/relocation_planner/country_detail.html index 1c896fa..547565a 100644 --- a/relocation_planner/templates/relocation_planner/country_detail.html +++ b/relocation_planner/templates/relocation_planner/country_detail.html @@ -22,12 +22,6 @@

-
- -
@@ -132,10 +126,6 @@

{{ visa.name }}

onclick="showRequirement('{{ req.name|escapejs }}', '{{ req.description|escapejs }}')" class="text-blue-600 hover:text-blue-800 hover:underline inline-flex items-center"> {{ req.name }} - - - - {% endfor %} @@ -164,8 +154,8 @@

{{ visa.name }}

{# Pet Relocation Card #}

Pet Relocation Requirements

- {% if country.pet_relocation_requirements.exists %} - {% regroup country.pet_relocation_requirements.all by animal as animal_list %} + {% if country.pet_requirement_list.exists %} + {% regroup country.pet_requirement_list.all by animal as animal_list %}
{% for animal in animal_list %}
diff --git a/relocation_planner/templates/relocation_planner/edit_country.html b/relocation_planner/templates/relocation_planner/edit_country.html index 614ca9d..6eb4e9e 100644 --- a/relocation_planner/templates/relocation_planner/edit_country.html +++ b/relocation_planner/templates/relocation_planner/edit_country.html @@ -117,6 +117,60 @@

Requirements

+ {# Pet Relocation Requirements #} +
+

Pet Relocation Requirements

+ {{ pet_requirement_formset.management_form }} + +
+ {% for form in pet_requirement_formset %} +
+

Requirement {{ forloop.counter }}

+ + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + +
+
+ + {{ form.animal }} +
+
+ + {{ form.type }} +
+
+ + {{ form.name }} +
+
+ + {{ form.duration }} +
+
+ + {{ form.description }} +
+
+ + {{ form.DELETE }} + +
+ {% endfor %} +
+ + +
+
@@ -279,5 +333,67 @@

Requirements

reqEntry.style.display = 'none'; } } + + function addPetRequirement() { + const forms = document.getElementById('pet-requirement-forms'); + const totalForms = document.querySelector('[name="pet_requirements-TOTAL_FORMS"]'); + const newFormIdx = totalForms.value; + + // Debug logs + console.log("Adding new pet requirement form"); + console.log("Current total forms:", newFormIdx); + + const template = ` +
+

New Requirement

+ + {% for hidden in pet_requirement_formset.empty_form.hidden_fields %} + {{ hidden.as_widget|escapejs }} + {% endfor %} + +
+
+ + {{ pet_requirement_formset.empty_form.animal.as_widget|escapejs }} +
+
+ + {{ pet_requirement_formset.empty_form.type.as_widget|escapejs }} +
+
+ + {{ pet_requirement_formset.empty_form.name.as_widget|escapejs }} +
+
+ + {{ pet_requirement_formset.empty_form.duration.as_widget|escapejs }} +
+
+ + {{ pet_requirement_formset.empty_form.description.as_widget|escapejs }} +
+
+ + + +
+ `.replace(/__prefix__/g, newFormIdx); + + forms.insertAdjacentHTML('beforeend', template); + totalForms.value = parseInt(totalForms.value) + 1; + } + + function removePetRequirement(button) { + const reqEntry = button.closest('.pet-requirement-entry'); + const deleteInput = reqEntry.querySelector('input[name$="-DELETE"]'); + if (deleteInput) { + deleteInput.value = 'on'; + reqEntry.style.display = 'none'; + } + } {% endblock %} \ No newline at end of file diff --git a/relocation_planner/views.py b/relocation_planner/views.py index ba87f35..a29fa0d 100644 --- a/relocation_planner/views.py +++ b/relocation_planner/views.py @@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.views import View from .forms.edit_country import EditCountryForm +from .forms.pet_relocation_requirement_formset import PetRelocationRequirementFormSet from .forms.visa_formset import VisaFormSet, VisaRequirementFormSet from .models import ( Country, @@ -37,8 +38,8 @@ def get(self, request, slug): 'common_languages', 'visa_list', 'visa_list__requirement_list', - 'pet_relocation_requirements', - 'pet_relocation_requirements__animal' + 'pet_requirement_list', + 'pet_requirement_list__animal' ).select_related('business_language'), slug=slug ) @@ -46,167 +47,6 @@ def get(self, request, slug): "country": country }) -# class EditCountryView(View): -# def get(self, request, slug=None): -# logger.debug(f"GET request received for slug: {slug}") -# try: -# country = None -# if slug: -# country = get_object_or_404(Country, slug=slug) - -# context = { -# "country": country, -# "languages": Language.objects.all().order_by('name'), -# "visas": Visa.objects.all().order_by('name'), -# "pet_requirements": PetRelocationRequirement.objects.all().order_by('animal__name', 'type'), -# "animals": AnimalSpecies.objects.all().order_by('name'), -# } - -# return render(request, "relocation_planner/edit_country.html", context) -# except Exception as e: -# logger.error(f"Error in GET: {str(e)}") -# raise - -# def post(self, request, slug=None): -# logger.debug("POST data received:") -# for key, value in request.POST.items(): -# logger.debug(f"{key}: {value}") - -# try: -# # Start a transaction since we're handling multiple related models -# with transaction.atomic(): -# if slug: -# country = get_object_or_404(Country, slug=slug) -# else: -# country = Country() - -# # Update basic country fields as before... -# country.name = request.POST.get("name") -# country.cost_of_living_index = request.POST.get("cost_of_living_index") or None -# country.quality_of_life_index = request.POST.get("quality_of_life_index") or None -# country.has_universal_healthcare = request.POST.get("has_universal_healthcare") == "on" -# country.pet_relocation_info_link = request.POST.get("pet_relocation_info_link", "") -# country.business_language_id = request.POST.get("business_language") or None - -# # Set the user who made the changes -# if not country.id: -# country.created_by = request.user -# country.last_modified_by = request.user - -# country.save() - -# # Update many-to-many relationships -# common_languages_ids = request.POST.getlist("common_languages") -# country.common_languages.set(common_languages_ids) - -# # Handle existing visas -# visa_ids = request.POST.getlist("visa_ids[]") -# existing_visas = set() # Keep track of visas we're updating - -# for visa_id in visa_ids: -# logger.debug(f"Processing visa {visa_id}") - -# try: -# visa = Visa.objects.get(id=visa_id) -# existing_visas.add(visa.id) - -# # Update visa fields -# visa.name = request.POST.get(f"visa_name_{visa_id}") -# visa.duration = request.POST.get(f"visa_duration_{visa_id}") -# visa.description = request.POST.get(f"visa_description_{visa_id}") -# visa.information_link = request.POST.get(f"visa_information_link_{visa_id}") -# visa.last_modified_by = request.user -# visa.save() - -# # Handle existing requirements -# req_ids = request.POST.getlist(f"visa_requirement_ids_{visa_id}[]", []) -# logger.debug(f"Found existing requirement IDs: {req_ids}") -# existing_reqs = set() - -# for req_id in req_ids: -# try: -# req = VisaRequirement.objects.get(id=req_id) -# existing_reqs.add(req.id) - -# req.name = request.POST.get(f"visa_requirement_name_{visa_id}_{req_id}") -# req.description = request.POST.get(f"visa_requirement_description_{visa_id}_{req_id}") -# req.last_modified_by = request.user -# req.save() -# except VisaRequirement.DoesNotExist: -# logger.warning(f"Requirement {req_id} not found for visa {visa_id}") - -# # Handle new requirements for existing visa -# # These come from the "Add Requirement" button in the form -# requirement_names = request.POST.getlist(f"new_visa_requirement_name_{visa_id}[]", []) -# requirement_descriptions = request.POST.getlist(f"new_visa_requirement_description_{visa_id}[]", []) - -# logger.debug(f"New requirements for visa {visa_id}: Names={requirement_names}, Descriptions={requirement_descriptions}") - -# for name, description in zip(requirement_names, requirement_descriptions): -# if name.strip(): # Only create if name is provided and not just whitespace -# new_req = VisaRequirement.objects.create( -# visa=visa, -# name=name, -# description=description, -# created_by=request.user, -# last_modified_by=request.user -# ) -# existing_reqs.add(new_req.id) -# logger.debug(f"Created new requirement: {new_req.id} for visa {visa_id}") - -# # Clean up removed requirements -# visa.requirements.exclude(id__in=existing_reqs).delete() - -# except Visa.DoesNotExist: -# messages.warning(request, f"Visa with ID {visa_id} not found") - -# # Handle new visas and their requirements (this part seems to be working) -# new_visa_names = request.POST.getlist("new_visa_name[]") -# new_visa_durations = request.POST.getlist("new_visa_duration[]") -# new_visa_descriptions = request.POST.getlist("new_visa_description[]") -# new_visa_links = request.POST.getlist("new_visa_information_link[]") - -# for i in range(len(new_visa_names)): -# if new_visa_names[i]: # Only create if name is provided -# visa = Visa.objects.create( -# name=new_visa_names[i], -# duration=new_visa_durations[i], -# description=new_visa_descriptions[i], -# information_link=new_visa_links[i], -# created_by=request.user, -# last_modified_by=request.user -# ) -# country.visas.add(visa) -# existing_visas.add(visa.id) - -# # Handle requirements for the new visa -# new_req_names = request.POST.getlist(f"new_requirement_name_new_{i}[]") -# new_req_descriptions = request.POST.getlist(f"new_requirement_description_new_{i}[]") -# logger.debug(f"Found new requirements for visa {visa_id}:") -# logger.debug(f"Names: {new_req_names}") -# logger.debug(f"Descriptions: {new_req_descriptions}") - -# for name, desc in zip(new_req_names, new_req_descriptions): -# if name.strip(): # Only create if name is provided and not just whitespace -# VisaRequirement.objects.create( -# visa=visa, -# name=name, -# description=desc, -# created_by=request.user, -# last_modified_by=request.user -# ) - -# # Clean up visas that were removed -# country.visas.exclude(id__in=existing_visas).delete() - -# messages.success(request, f"Country {'updated' if slug else 'added'} successfully!") -# return redirect("relocation_planner:country_detail", slug=country.slug) - -# except Exception as e: -# logger.error(f"Error saving country: {str(e)}") -# messages.error(request, f"An error occurred: {str(e)}") -# return self.get(request, slug) - class EditCountryView(View): template_name = "relocation_planner/edit_country.html" @@ -229,12 +69,17 @@ def get(self, request, slug=None): requirement_formsets = self.get_visa_requirement_formsets( country.visa_list.all() if country else [] ) + pet_requirement_formset = PetRelocationRequirementFormSet( + instance=country if country else None, + prefix='pet_requirements' + ) context = { "country": country, "country_form": country_form, "visa_formset": visa_formset, "requirement_formsets": requirement_formsets, + "pet_requirement_formset": pet_requirement_formset, } return render(request, self.template_name, context) @@ -245,14 +90,26 @@ def post(self, request, slug=None): country = get_object_or_404(Country, slug=slug) if slug else None country_form = EditCountryForm(request.POST, instance=country) visa_formset = VisaFormSet(request.POST, instance=country if country else None, prefix='visas') + pet_requirement_formset = PetRelocationRequirementFormSet( + request.POST, + instance=country if country else None, + prefix='pet_requirements' + ) print(f"Country form valid: {country_form.is_valid()}") # Debug print(f"Visa formset valid: {visa_formset.is_valid()}") # Debug if not visa_formset.is_valid(): print(f"Visa formset errors: {visa_formset.errors}") # Debug + print(f"Pet requirement formset valid: {pet_requirement_formset.is_valid()}") # Debug + if not pet_requirement_formset.is_valid(): + print(f"Pet requirement formset errors: {pet_requirement_formset.errors}") # Debug requirement_formsets = {} - is_valid = country_form.is_valid() and visa_formset.is_valid() + is_valid = ( + country_form.is_valid() + and visa_formset.is_valid() + and pet_requirement_formset.is_valid() + ) # Process existing visas and collect their requirement formsets if is_valid: @@ -308,6 +165,17 @@ def post(self, request, slug=None): if visa_form.instance.pk: visa_form.instance.delete() + # Save pet requirements + for req_form in pet_requirement_formset.forms: + if req_form.is_valid() and not req_form.cleaned_data.get('DELETE', False): + requirement = req_form.save(commit=False) + requirement.country = country + requirement.save() + else: + # Handle deletion + if req_form.instance.pk and req_form.cleaned_data.get('DELETE', False): + req_form.instance.delete() + return redirect("relocation_planner:country_detail", slug=country.slug) # If we get here, there was a validation error @@ -321,6 +189,7 @@ def post(self, request, slug=None): "country_form": country_form, "visa_formset": visa_formset, "requirement_formsets": requirement_formsets, + "pet_requirement_formset": pet_requirement_formset, } return render(request, self.template_name, context) From 6d774bc04f7c62381e12093922aa6658f8fac2fb Mon Sep 17 00:00:00 2001 From: Brent Mills Date: Tue, 26 Nov 2024 23:47:16 -0500 Subject: [PATCH 7/8] Resolve issue with new visas --- relocation_planner/views.py | 137 +++++++++++++++++------------------- 1 file changed, 66 insertions(+), 71 deletions(-) diff --git a/relocation_planner/views.py b/relocation_planner/views.py index a29fa0d..708119a 100644 --- a/relocation_planner/views.py +++ b/relocation_planner/views.py @@ -54,7 +54,7 @@ def get_visa_requirement_formsets(self, visas): requirement_formsets = {} for visa in visas: visa_id = visa.id if visa.id else f'new_{uuid.uuid4().hex}' - prefix = f'visa_{visa_id}_requirements' # Match the JS prefix + prefix = f'visa_{visa_id}_requirements' formset = VisaRequirementFormSet( instance=visa, prefix=prefix @@ -84,8 +84,8 @@ def get(self, request, slug=None): return render(request, self.template_name, context) def post(self, request, slug=None): - print("POST received") # Debug - print("POST data keys:", request.POST.keys()) # Debug + print("POST received") + print("POST data keys:", request.POST.keys()) country = get_object_or_404(Country, slug=slug) if slug else None country_form = EditCountryForm(request.POST, instance=country) @@ -95,90 +95,85 @@ def post(self, request, slug=None): instance=country if country else None, prefix='pet_requirements' ) - - print(f"Country form valid: {country_form.is_valid()}") # Debug - print(f"Visa formset valid: {visa_formset.is_valid()}") # Debug - if not visa_formset.is_valid(): - print(f"Visa formset errors: {visa_formset.errors}") # Debug - print(f"Pet requirement formset valid: {pet_requirement_formset.is_valid()}") # Debug - if not pet_requirement_formset.is_valid(): - print(f"Pet requirement formset errors: {pet_requirement_formset.errors}") # Debug - requirement_formsets = {} - is_valid = ( - country_form.is_valid() - and visa_formset.is_valid() - and pet_requirement_formset.is_valid() - ) + # First validate the main forms + country_form_valid = country_form.is_valid() + visa_formset_valid = visa_formset.is_valid() + pet_requirement_valid = pet_requirement_formset.is_valid() - # Process existing visas and collect their requirement formsets - if is_valid: - for visa_form in visa_formset.forms: - if not visa_form.cleaned_data.get('DELETE', False): - visa = visa_form.instance - visa_id = visa.id if visa.id else f'new_{uuid.uuid4().hex}' - - # Get the correct prefix for requirements - prefix = f'visa_{visa_id}_requirements' + print(f"Country form valid: {country_form_valid}") + print(f"Visa formset valid: {visa_formset_valid}") + print(f"Pet requirement formset valid: {pet_requirement_valid}") - print(f"Looking for requirements with prefix: {prefix}") # Debug - requirement_keys = [k for k in request.POST.keys() if k.startswith(prefix)] # Debug - print(f"Found requirement keys: {requirement_keys}") # Debug - - # Create requirement formset with the correct prefix - requirement_formset = VisaRequirementFormSet( - request.POST, - instance=visa, - prefix=prefix - ) - - if requirement_formset.is_valid(): - requirement_formsets[visa_id] = requirement_formset - else: - is_valid = False - requirement_formsets[visa_id] = requirement_formset + if not visa_formset_valid: + print(f"Visa formset errors: {visa_formset.errors}") + if not pet_requirement_valid: + print(f"Pet requirement formset errors: {pet_requirement_formset.errors}") + + # Process and validate visa requirements if main forms are valid + requirement_formsets = {} + visa_requirements_valid = True - if is_valid: + if country_form_valid and visa_formset_valid and pet_requirement_valid: + # Save the country first to ensure it exists for relationships country = country_form.save() - - # Save visas + + # Process each visa and its requirements for visa_form in visa_formset.forms: if not visa_form.cleaned_data.get('DELETE', False): + # Save the visa first visa = visa_form.save(commit=False) visa.country = country visa.save() - - # Get the corresponding requirement formset - visa_id = visa.id if visa.id else f'new_{uuid.uuid4().hex}' - requirement_formset = requirement_formsets.get(visa_id) - - if requirement_formset: - # Save requirements - requirements = requirement_formset.save(commit=False) - for req in requirement_formset.deleted_objects: - req.delete() - for requirement in requirements: - requirement.visa = visa - requirement.save() + + # Now handle requirements for this visa + visa_id = str(visa.id) if visa.id else f'new_{uuid.uuid4().hex}' + prefix = f'visa_{visa_id}_requirements' + + print(f"Looking for requirements with prefix: {prefix}") + requirement_keys = [k for k in request.POST.keys() if k.startswith(prefix)] + print(f"Found requirement keys: {requirement_keys}") + + if requirement_keys: + requirement_formset = VisaRequirementFormSet( + request.POST, + instance=visa, + prefix=prefix + ) + + if requirement_formset.is_valid(): + print(f"Processing requirements for visa {visa_id}") + # Save requirements + requirements = requirement_formset.save(commit=False) + for requirement in requirements: + requirement.visa = visa + requirement.save() + + # Handle deletions + for obj in requirement_formset.deleted_objects: + obj.delete() + else: + print(f"Requirement formset errors for visa {visa_id}: {requirement_formset.errors}") + visa_requirements_valid = False else: - # Handle deletion + # Handle visa deletion if visa_form.instance.pk: visa_form.instance.delete() - # Save pet requirements - for req_form in pet_requirement_formset.forms: - if req_form.is_valid() and not req_form.cleaned_data.get('DELETE', False): - requirement = req_form.save(commit=False) - requirement.country = country - requirement.save() - else: - # Handle deletion - if req_form.instance.pk and req_form.cleaned_data.get('DELETE', False): - req_form.instance.delete() + # Process pet requirements if everything else is valid + if visa_requirements_valid: + for req_form in pet_requirement_formset.forms: + if not req_form.cleaned_data.get('DELETE', False): + requirement = req_form.save(commit=False) + requirement.country = country + requirement.save() + else: + if req_form.instance.pk: + req_form.instance.delete() - return redirect("relocation_planner:country_detail", slug=country.slug) + return redirect("relocation_planner:country_detail", slug=country.slug) - # If we get here, there was a validation error + # If we get here, something failed validation if not requirement_formsets: requirement_formsets = self.get_visa_requirement_formsets( [form.instance for form in visa_formset.forms if form.instance.pk and not form.cleaned_data.get('DELETE', False)] From cb91e605ac70269e474e3c3cbdfc466964881175 Mon Sep 17 00:00:00 2001 From: Brent Mills Date: Tue, 26 Nov 2024 23:55:36 -0500 Subject: [PATCH 8/8] Add admin support --- relocation_planner/admin.py | 57 ++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/relocation_planner/admin.py b/relocation_planner/admin.py index acf1dc6..e41f321 100644 --- a/relocation_planner/admin.py +++ b/relocation_planner/admin.py @@ -1,5 +1,49 @@ from django.contrib import admin -from .models import AnimalSpecies, PetRelocationRequirement +from .models import ( + Country, + Language, + Visa, + VisaRequirement, + AnimalSpecies, + PetRelocationRequirement +) + +# Inline admin classes for nested relationships +class VisaRequirementInline(admin.TabularInline): + model = VisaRequirement + extra = 1 + +class VisaInline(admin.StackedInline): + model = Visa + extra = 1 + inlines = [VisaRequirementInline] + show_change_link = True + +class PetRelocationRequirementInline(admin.TabularInline): + model = PetRelocationRequirement + extra = 1 + +@admin.register(Country) +class CountryAdmin(admin.ModelAdmin): + list_display = ('name', 'business_language', 'cost_of_living_index', 'quality_of_life_index') + list_filter = ('has_universal_healthcare', 'business_language') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + filter_horizontal = ('common_languages',) + inlines = [VisaInline, PetRelocationRequirementInline] + +@admin.register(Visa) +class VisaAdmin(admin.ModelAdmin): + list_display = ('name', 'country', 'duration') + list_filter = ('country',) + search_fields = ('name', 'country__name') + inlines = [VisaRequirementInline] + +@admin.register(VisaRequirement) +class VisaRequirementAdmin(admin.ModelAdmin): + list_display = ('name', 'visa') + list_filter = ('visa__country',) + search_fields = ('name', 'description', 'visa__name') @admin.register(AnimalSpecies) class AnimalSpeciesAdmin(admin.ModelAdmin): @@ -8,6 +52,11 @@ class AnimalSpeciesAdmin(admin.ModelAdmin): @admin.register(PetRelocationRequirement) class PetRelocationRequirementAdmin(admin.ModelAdmin): - list_display = ('name', 'animal', 'type', 'duration') - list_filter = ('animal', 'type') - search_fields = ('name', 'description') \ No newline at end of file + list_display = ('name', 'animal', 'type', 'country') + list_filter = ('animal', 'type', 'country') + search_fields = ('name', 'description', 'country__name') + +@admin.register(Language) +class LanguageAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ('name',) \ No newline at end of file