diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..eaf2d70 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..e14328a 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..43d0c77 100644 --- a/config/settings.py +++ b/config/settings.py @@ -180,3 +180,7 @@ if EMAIL_USE_SSL: # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Media files (User uploads) +MEDIA_URL = 'media/' +MEDIA_ROOT = BASE_DIR / 'media' diff --git a/config/urls.py b/config/urls.py index bcfc074..1a48858 100644 --- a/config/urls.py +++ b/config/urls.py @@ -27,3 +27,4 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..62a7133 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..efbc4ce 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..e5c2e74 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..15ad71e 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..d64dec8 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,16 @@ from django.contrib import admin +from .models import Intent, ValueTag, Profile + +@admin.register(Intent) +class IntentAdmin(admin.ModelAdmin): + list_display = ('name', 'icon') + +@admin.register(ValueTag) +class ValueTagAdmin(admin.ModelAdmin): + list_display = ('name',) + +@admin.register(Profile) +class ProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'professional_headline', 'transition_status', 'location_city') + filter_horizontal = ('intents', 'value_tags') -# Register your models here. diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..d4f8512 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 5.2.7 on 2026-02-17 16:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Intent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('icon', models.CharField(blank=True, help_text='Bootstrap icon class name', max_length=50)), + ], + ), + migrations.CreateModel( + name='ValueTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('professional_headline', models.CharField(blank=True, max_length=255)), + ('transition_status', models.CharField(choices=[('none', 'Stable'), ('post-divorce', 'Post-Divorce'), ('relocating', 'Relocating'), ('career-change', 'Career Change'), ('new-in-town', 'New in Town')], default='none', max_length=50)), + ('bio', models.TextField(blank=True)), + ('location_city', models.CharField(blank=True, max_length=100)), + ('avatar_url', models.URLField(blank=True, null=True)), + ('intents', models.ManyToManyField(blank=True, to='core.intent')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('value_tags', models.ManyToManyField(blank=True, to='core.valuetag')), + ], + ), + ] diff --git a/core/migrations/0002_profile_is_email_verified_and_more.py b/core/migrations/0002_profile_is_email_verified_and_more.py new file mode 100644 index 0000000..f9eff0a --- /dev/null +++ b/core/migrations/0002_profile_is_email_verified_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.7 on 2026-02-17 16:47 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='is_email_verified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profile', + name='onboarding_completed', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profile', + name='organization_id', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='profile', + name='two_factor_enabled', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='profile', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/0003_group_event_report.py b/core/migrations/0003_group_event_report.py new file mode 100644 index 0000000..0f34302 --- /dev/null +++ b/core/migrations/0003_group_event_report.py @@ -0,0 +1,55 @@ +# Generated by Django 5.2.7 on 2026-02-17 16:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_profile_is_email_verified_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('description', models.TextField()), + ('is_private', models.BooleanField(default=False)), + ('organization_id', models.IntegerField(blank=True, null=True)), + ('members', models.ManyToManyField(related_name='joined_groups', to=settings.AUTH_USER_MODEL)), + ('moderators', models.ManyToManyField(related_name='moderated_groups', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ('location', models.CharField(max_length=255)), + ('organization_id', models.IntegerField(blank=True, null=True)), + ('attendees', models.ManyToManyField(related_name='attending_events', to=settings.AUTH_USER_MODEL)), + ('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='core.group')), + ], + ), + migrations.CreateModel( + name='Report', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('resolved', models.BooleanField(default=False)), + ('organization_id', models.IntegerField(blank=True, null=True)), + ('reported_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reports_received', to=settings.AUTH_USER_MODEL)), + ('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports_made', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0004_message.py b/core/migrations/0004_message.py new file mode 100644 index 0000000..854d9c8 --- /dev/null +++ b/core/migrations/0004_message.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2026-02-17 16:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_group_event_report'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', models.TextField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('is_read', models.BooleanField(default=False)), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['timestamp'], + }, + ), + ] diff --git a/core/migrations/0005_remove_profile_avatar_url_profile_avatar.py b/core/migrations/0005_remove_profile_avatar_url_profile_avatar.py new file mode 100644 index 0000000..32bf541 --- /dev/null +++ b/core/migrations/0005_remove_profile_avatar_url_profile_avatar.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.7 on 2026-02-17 17:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_message'), + ] + + operations = [ + migrations.RemoveField( + model_name='profile', + name='avatar_url', + ), + migrations.AddField( + model_name='profile', + name='avatar', + field=models.ImageField(blank=True, null=True, upload_to='avatars/'), + ), + ] diff --git a/core/migrations/0006_post_comment_hiddenpost_reaction.py b/core/migrations/0006_post_comment_hiddenpost_reaction.py new file mode 100644 index 0000000..c3faf35 --- /dev/null +++ b/core/migrations/0006_post_comment_hiddenpost_reaction.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.7 on 2026-02-17 17:06 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_remove_profile_avatar_url_profile_avatar'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('image', models.ImageField(blank=True, null=True, upload_to='posts/')), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='core.post')), + ], + options={ + 'ordering': ['timestamp'], + }, + ), + migrations.CreateModel( + name='HiddenPost', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hidden_posts', to=settings.AUTH_USER_MODEL)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.post')), + ], + options={ + 'unique_together': {('user', 'post')}, + }, + ), + migrations.CreateModel( + name='Reaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reaction_type', models.CharField(choices=[('heart', 'Heart'), ('like', 'Like'), ('smile', 'Smile')], default='heart', max_length=20)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='core.post')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('post', 'user', 'reaction_type')}, + }, + ), + ] diff --git a/core/migrations/0007_post_post_type_profile_accountability_streak_and_more.py b/core/migrations/0007_post_post_type_profile_accountability_streak_and_more.py new file mode 100644 index 0000000..f1c1b20 --- /dev/null +++ b/core/migrations/0007_post_post_type_profile_accountability_streak_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.7 on 2026-02-17 17:17 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_post_comment_hiddenpost_reaction'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='post_type', + field=models.CharField(choices=[('reflection', 'Reflection'), ('looking_for', 'Looking For'), ('offering', 'Offering'), ('event_invite', 'Event Invite'), ('progress_update', 'Progress Update'), ('skill_share', 'Skill Share')], default='reflection', max_length=20), + ), + migrations.AddField( + model_name='profile', + name='accountability_streak', + field=models.IntegerField(default=0), + ), + migrations.CreateModel( + name='Connection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user1', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections1', to=settings.AUTH_USER_MODEL)), + ('user2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connections2', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user1', 'user2')}, + }, + ), + ] diff --git a/core/migrations/0008_follow.py b/core/migrations/0008_follow.py new file mode 100644 index 0000000..126f76c --- /dev/null +++ b/core/migrations/0008_follow.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-17 17:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_post_post_type_profile_accountability_streak_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Follow', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('followed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_relations', to=settings.AUTH_USER_MODEL)), + ('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_relations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('follower', 'followed')}, + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..30b7ef4 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_profile_is_email_verified_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_profile_is_email_verified_and_more.cpython-311.pyc new file mode 100644 index 0000000..e4ad9a2 Binary files /dev/null and b/core/migrations/__pycache__/0002_profile_is_email_verified_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_group_event_report.cpython-311.pyc b/core/migrations/__pycache__/0003_group_event_report.cpython-311.pyc new file mode 100644 index 0000000..f9a82dd Binary files /dev/null and b/core/migrations/__pycache__/0003_group_event_report.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_message.cpython-311.pyc b/core/migrations/__pycache__/0004_message.cpython-311.pyc new file mode 100644 index 0000000..e2a6337 Binary files /dev/null and b/core/migrations/__pycache__/0004_message.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0005_remove_profile_avatar_url_profile_avatar.cpython-311.pyc b/core/migrations/__pycache__/0005_remove_profile_avatar_url_profile_avatar.cpython-311.pyc new file mode 100644 index 0000000..2c59b61 Binary files /dev/null and b/core/migrations/__pycache__/0005_remove_profile_avatar_url_profile_avatar.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0006_post_comment_hiddenpost_reaction.cpython-311.pyc b/core/migrations/__pycache__/0006_post_comment_hiddenpost_reaction.cpython-311.pyc new file mode 100644 index 0000000..afa4e1d Binary files /dev/null and b/core/migrations/__pycache__/0006_post_comment_hiddenpost_reaction.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0007_post_post_type_profile_accountability_streak_and_more.cpython-311.pyc b/core/migrations/__pycache__/0007_post_post_type_profile_accountability_streak_and_more.cpython-311.pyc new file mode 100644 index 0000000..60740c2 Binary files /dev/null and b/core/migrations/__pycache__/0007_post_post_type_profile_accountability_streak_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0008_follow.cpython-311.pyc b/core/migrations/__pycache__/0008_follow.cpython-311.pyc new file mode 100644 index 0000000..6456005 Binary files /dev/null and b/core/migrations/__pycache__/0008_follow.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..a5db98c 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,212 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. +class Intent(models.Model): + name = models.CharField(max_length=100) + icon = models.CharField(max_length=50, blank=True, help_text="Bootstrap icon class name") + + def __str__(self): + return str(self.name) + +class ValueTag(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return str(self.name) + +class Profile(models.Model): + TRANSITION_CHOICES = [ + ('none', 'Stable'), + ('post-divorce', 'Post-Divorce'), + ('relocating', 'Relocating'), + ('career-change', 'Career Change'), + ('new-in-town', 'New in Town'), + ] + + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + professional_headline = models.CharField(max_length=255, blank=True) + transition_status = models.CharField(max_length=50, choices=TRANSITION_CHOICES, default='none') + bio = models.TextField(blank=True) + location_city = models.CharField(max_length=100, blank=True) + intents = models.ManyToManyField(Intent, blank=True) + value_tags = models.ManyToManyField(ValueTag, blank=True) + avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) + + # Momentum & Engagement + accountability_streak = models.IntegerField(default=0) + + # Auth & Security + is_email_verified = models.BooleanField(default=False) + two_factor_enabled = models.BooleanField(default=False) + onboarding_completed = models.BooleanField(default=False) + + # Multitenancy (Future Proofing) + organization_id = models.IntegerField(null=True, blank=True) + + @property + def get_avatar_url(self): + if self.avatar and hasattr(self.avatar, 'url'): + return self.avatar.url + return f"https://i.pravatar.cc/150?u={self.user.username}" + + @property + def connection_count(self): + return Connection.objects.filter(models.Q(user1=self.user) | models.Q(user2=self.user)).count() + + @property + def following_count(self): + return Follow.objects.filter(follower=self.user).count() + + @property + def followers_count(self): + return Follow.objects.filter(followed=self.user).count() + + @property + def events_attended_count(self): + return self.user.attending_events.count() + + @property + def profile_completion_percentage(self): + steps = 0 + total_steps = 5 + if self.professional_headline: steps += 1 + if self.bio: steps += 1 + if self.location_city: steps += 1 + if self.intents.exists(): steps += 1 + if self.avatar: steps += 1 + return int((steps / total_steps) * 100) + + def __str__(self): + return f"{self.user.username}'s Profile" + +class Connection(models.Model): + user1 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='connections1') + user2 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='connections2') + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('user1', 'user2') + + def __str__(self): + return f"{self.user1.username} <-> {self.user2.username}" + +class Follow(models.Model): + follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name='following_relations') + followed = models.ForeignKey(User, on_delete=models.CASCADE, related_name='follower_relations') + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('follower', 'followed') + + def __str__(self): + return f"{self.follower.username} follows {self.followed.username}" + +class Group(models.Model): + name = models.CharField(max_length=255) + description = models.TextField() + is_private = models.BooleanField(default=False) + moderators = models.ManyToManyField(User, related_name='moderated_groups') + members = models.ManyToManyField(User, related_name='joined_groups') + organization_id = models.IntegerField(null=True, blank=True) + + def __str__(self): + return str(self.name) + +class Event(models.Model): + title = models.CharField(max_length=255) + description = models.TextField() + start_time = models.DateTimeField() + end_time = models.DateTimeField() + location = models.CharField(max_length=255) + group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name='events', null=True, blank=True) + organizer = models.ForeignKey(User, on_delete=models.CASCADE) + attendees = models.ManyToManyField(User, related_name='attending_events') + organization_id = models.IntegerField(null=True, blank=True) + + def __str__(self): + return str(self.title) + +class Report(models.Model): + reporter = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reports_made') + reported_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reports_received', null=True, blank=True) + content = models.TextField() + timestamp = models.DateTimeField(auto_now_add=True) + resolved = models.BooleanField(default=False) + organization_id = models.IntegerField(null=True, blank=True) + + def __str__(self): + return f"Report by {self.reporter.username} at {self.timestamp}" + +class Message(models.Model): + sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages') + recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages') + body = models.TextField() + timestamp = models.DateTimeField(auto_now_add=True) + is_read = models.BooleanField(default=False) + + class Meta: + ordering = ['timestamp'] + + def __str__(self): + return f"From {self.sender.username} to {self.recipient.username} at {self.timestamp}" + +class Post(models.Model): + POST_TYPE_CHOICES = [ + ('reflection', 'Reflection'), + ('looking_for', 'Looking For'), + ('offering', 'Offering'), + ('event_invite', 'Event Invite'), + ('progress_update', 'Progress Update'), + ('skill_share', 'Skill Share'), + ] + author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts') + content = models.TextField() + image = models.ImageField(upload_to='posts/', blank=True, null=True) + post_type = models.CharField(max_length=20, choices=POST_TYPE_CHOICES, default='reflection') + timestamp = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-timestamp'] + + def __str__(self): + return f"Post by {self.author.username} at {self.timestamp}" + + def user_has_reacted(self, user, reaction_type='heart'): + if user.is_authenticated: + return self.reactions.filter(user=user, reaction_type=reaction_type).exists() + return False + +class Comment(models.Model): + post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments') + author = models.ForeignKey(User, on_delete=models.CASCADE) + content = models.TextField() + timestamp = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['timestamp'] + + def __str__(self): + return f"Comment by {self.author.username} on {self.post}" + +class Reaction(models.Model): + REACTION_CHOICES = [ + ('heart', 'Heart'), + ('like', 'Like'), + ('smile', 'Smile'), + ] + post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='reactions') + user = models.ForeignKey(User, on_delete=models.CASCADE) + reaction_type = models.CharField(max_length=20, choices=REACTION_CHOICES, default='heart') + + class Meta: + unique_together = ('post', 'user', 'reaction_type') + + def __str__(self): + return f"{self.user.username} {self.reaction_type}ed {self.post}" + +class HiddenPost(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hidden_posts') + post = models.ForeignKey(Post, on_delete=models.CASCADE) + + class Meta: + unique_together = ('user', 'post') diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..0856475 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -3,23 +3,98 @@
-