Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions backend/authentication/admin.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from authentication.models import CustomUser


class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = (
"email",
"role",
"is_staff",
"is_active",
)
list_filter = (
"email",
"role",
"is_staff",
"is_active",
)
fieldsets = (
(None, {"fields": ("email", "password")}),
("Permissions", {"fields": ("is_staff", "is_active")}),
("Permissions", {"fields": ("role", "is_staff", "is_active")}),
)
add_fieldsets = (
(None, {"classes": ("wide",), "fields": ("email", "password1", "password2", "is_staff", "is_active")}),
(
None,
{
"classes": ("wide",),
"fields": ("email", "password1", "password2", "role", "is_staff", "is_active"),
},
),
)
search_fields = ("email",)
ordering = ("email",)
Expand All @@ -30,12 +37,10 @@ class CustomUserAdmin(UserAdmin):

def make_active(self, request, queryset):
queryset.update(is_active=True)

make_active.short_description = "Mark selected users as active"

def make_inactive(self, request, queryset):
queryset.update(is_active=False)

make_inactive.short_description = "Mark selected users as inactive"


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-07-05 10:06

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('authentication', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('admin', 'Admin'), ('staff', 'Staff'), ('uploader', 'Uploader'), ('viewer', 'Viewer')], default='viewer', max_length=20),
),
migrations.AlterField(
model_name='customuser',
name='is_active',
field=models.BooleanField(default=True),
),
]
29 changes: 17 additions & 12 deletions backend/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,42 @@


class CustomUserManager(BaseUserManager):
def create_user(self, email, password, **extra_fields):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("The Email field must be set")
if not password:
raise ValueError("The Password field must be set")

local, domain = email.split("@")
if "+" in local:
local = local.split("+")[0]
email = f"{local}@{domain.lower()}"

email = self.normalize_email(email)
extra_fields.setdefault("is_active", True)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)

return user

def create_superuser(self, email, password, **extra_fields):
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_active", True)
extra_fields.setdefault("is_superuser", True)

if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")

return self.create_user(email, password, **extra_fields)


class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
is_active = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)

ROLE_CHOICES = (
("admin", "Admin"),
("staff", "Staff"),
("uploader", "Uploader"),
("viewer", "Viewer"),
)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default="viewer")

objects = CustomUserManager()

USERNAME_FIELD = "email"
Expand Down
66 changes: 62 additions & 4 deletions backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@

load_dotenv()

GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

Expand Down Expand Up @@ -47,6 +54,8 @@
"authentication",
"chat",
"gpt",
'django_crontab',
'rest_framework_simplejwt',
]

MIDDLEWARE = [
Expand All @@ -59,9 +68,20 @@
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware",
]

CRONJOBS = [
('0 2 * * *', 'django.core.management.call_command', ['cleanup_conversations']),
]
ROOT_URLCONF = "backend.urls"

REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}


TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
Expand All @@ -85,12 +105,17 @@
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'fullstack_db',
'USER': 'postgres',
'PASSWORD': 'Pvinod@123',
'HOST': 'localhost',
'PORT': '5432',
}
}


# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

Expand Down Expand Up @@ -149,3 +174,36 @@
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "None"

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{asctime} [{levelname}] {name}: {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': BASE_DIR / 'file_activity.log',
'formatter': 'verbose',
},
},
'loggers': {
'file_activity': {
'handlers': ['file'],
'level': 'INFO',
'propagate': False,
},
},
}

CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': BASE_DIR / 'django_cache', # Directory to store cache files
}
}
10 changes: 9 additions & 1 deletion backend/backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.http import JsonResponse
from django.urls import include, path
from rest_framework.decorators import api_view
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView


@api_view(["GET"])
Expand All @@ -16,5 +17,12 @@ def root_view(request):
path("chat/", include("chat.urls")),
path("gpt/", include("gpt.urls")),
path("auth/", include("authentication.urls")),
path("api/", include("chat.urls")),
path("", root_view),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

# JWT Auth Endpoints
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
18 changes: 16 additions & 2 deletions backend/chat/admin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
from django.contrib import admin
from django.utils import timezone
from nested_admin.nested import NestedModelAdmin, NestedStackedInline, NestedTabularInline
from django.utils.html import format_html
from django.conf import settings

from chat.models import Conversation, Message, Role, Version
from chat.models import Conversation, Message, Role, Version, UploadedFile


@admin.register(UploadedFile)
class UploadedFileAdmin(admin.ModelAdmin):
list_display = ['file', 'conversation', 'uploaded_at', 'download_link']
list_filter = ['conversation']
search_fields = ['file', 'conversation__title']

def download_link(self, obj):
return format_html(
f"<a href='{settings.MEDIA_URL}{obj.file}' target='_blank'>Download</a>"
)
download_link.short_description = "Download"

class RoleAdmin(NestedModelAdmin):
list_display = ["id", "name"]

Expand Down Expand Up @@ -51,7 +65,7 @@ def queryset(self, request, queryset):
class ConversationAdmin(NestedModelAdmin):
actions = ["undelete_selected", "soft_delete_selected"]
inlines = [VersionInline]
list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user")
list_display = ("title","topic","id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user")
list_filter = (DeletedListFilter,)
ordering = ("-modified_at",)

Expand Down
2 changes: 2 additions & 0 deletions backend/chat/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
class ChatConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "chat"
def ready(self):
import chat.signals
14 changes: 14 additions & 0 deletions backend/chat/management/commands/cleanup_conversations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
from chat.models import Conversation

class Command(BaseCommand):
help = "Soft deletes conversations older than 30 days"

def handle(self, *args, **options):
threshold_date = timezone.now() - timedelta(days=30)
old_conversations = Conversation.objects.filter(created_at__lt=threshold_date, deleted_at__isnull=True)

count = old_conversations.update(deleted_at=timezone.now())
self.stdout.write(self.style.SUCCESS(f"{count} old conversations soft-deleted."))
17 changes: 17 additions & 0 deletions backend/chat/management/commands/populate_search_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.core.management.base import BaseCommand
from django.contrib.postgres.search import SearchVector
from chat.models import FileChunk

class Command(BaseCommand):
help = "Populate search_vector field in FileChunk"

def handle(self, *args, **kwargs):
chunks = FileChunk.objects.all()
count = 0

for chunk in chunks:
chunk.search_vector = SearchVector('content')
chunk.save()
count += 1

self.stdout.write(self.style.SUCCESS(f"Updated {count} FileChunks with search_vector"))
18 changes: 18 additions & 0 deletions backend/chat/migrations/0002_conversation_topic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-07-03 15:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('chat', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='conversation',
name='topic',
field=models.CharField(blank=True, max_length=100, null=True),
),
]
18 changes: 18 additions & 0 deletions backend/chat/migrations/0003_conversation_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-07-03 15:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('chat', '0002_conversation_topic'),
]

operations = [
migrations.AddField(
model_name='conversation',
name='summary',
field=models.TextField(blank=True, null=True),
),
]
21 changes: 21 additions & 0 deletions backend/chat/migrations/0004_uploadedfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.2.4 on 2025-07-03 18:24

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('chat', '0003_conversation_summary'),
]

operations = [
migrations.CreateModel(
name='UploadedFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to='uploads/')),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
],
),
]
Loading