diff --git a/.gitignore b/.gitignore index 2e2ba01e5..573b05d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ server/dist # prompts prompts/debug + +venv diff --git a/backend/authentication/admin.py b/backend/authentication/admin.py index 7dea0eceb..417736b14 100644 --- a/backend/authentication/admin.py +++ b/backend/authentication/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin +from .models import Conversation + from authentication.models import CustomUser @@ -40,3 +42,8 @@ def make_inactive(self, request, queryset): admin.site.register(CustomUser, CustomUserAdmin) + +class ConversationAdmin(admin.ModelAdmin): + list_display = ('id', 'summary') # Including the new field + +admin.site.register(Conversation, ConversationAdmin) \ No newline at end of file diff --git a/backend/authentication/migrations/0002_conversation.py b/backend/authentication/migrations/0002_conversation.py new file mode 100644 index 000000000..c46251dbd --- /dev/null +++ b/backend/authentication/migrations/0002_conversation.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.1 on 2024-09-07 17:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('summary', models.TextField(blank=True, null=True)), + ], + ), + ] diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 4a565e6cd..e9fe95a2a 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -41,3 +41,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): def __str__(self): return self.email +class Conversation(models.Model): + content = models.TextField() + summary = models.TextField(null=True, blank=True) # New field +class FileUpload(models.Model): + file = models.FileField(upload_to='uploads/') + uploaded_at = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/backend/authentication/permissions.py b/backend/authentication/permissions.py new file mode 100644 index 000000000..536def2d9 --- /dev/null +++ b/backend/authentication/permissions.py @@ -0,0 +1,10 @@ +from rest_framework.permissions import BasePermission + +class IsAdminOrReadOnly(BasePermission): + """ + Custom permission to only allow admin users to delete objects. + """ + def has_permission(self, request, view): + if request.method in ['GET', 'HEAD', 'OPTIONS']: + return True + return request.user and request.user.is_staff diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py new file mode 100644 index 000000000..447936fda --- /dev/null +++ b/backend/authentication/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from .models import Conversation, FileUpload + +class ConversationSerializer(serializers.ModelSerializer): + class Meta: + model = Conversation + fields = ['id', 'content', 'summary'] + +class FileUploadSerializer(serializers.ModelSerializer): + class Meta: + model = FileUpload + fields = ['id', 'file', 'uploaded_at'] diff --git a/backend/authentication/signals.py b/backend/authentication/signals.py new file mode 100644 index 000000000..298c3b8b0 --- /dev/null +++ b/backend/authentication/signals.py @@ -0,0 +1,11 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Conversation +from .utils import generate_summary # Import a utility function to generate summaries + +@receiver(post_save, sender=Conversation) +def generate_and_store_summary(sender, instance, **kwargs): + if kwargs.get('created', False): # this will only generate summary for new instances + summary = generate_summary(instance) + instance.summary = summary + instance.save() diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 93c863847..2135d80cb 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -1,7 +1,13 @@ from django.urls import path - +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import ConversationViewSet, FileUploadViewSet from authentication import views +router = DefaultRouter() +router.register(r'conversations', ConversationViewSet) +router.register(r'file-uploads', FileUploadViewSet) + urlpatterns = [ path("", views.auth_root_view, name="auth_root"), path("csrf_token/", views.csrf_token, name="csrf_token"), @@ -9,4 +15,6 @@ path("logout/", views.logout_view, name="logout"), path("register/", views.register_view, name="register"), path("verify_session/", views.verify_session, name="verify_session"), + path('', include(router.urls)), + ] diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 068100805..3bfcd1149 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -6,6 +6,71 @@ from rest_framework.decorators import api_view from authentication.models import CustomUser +from rest_framework import viewsets +from rest_framework.response import Response +from .models import Conversation, FileUpload +from .serializers import ConversationSerializer, FileUploadSerializer +from .permissions import IsAdminOrReadOnly +import logging +from django.core.cache import cache +from rest_framework.response import Response + +class ConversationViewSet(viewsets.ModelViewSet): + queryset = Conversation.objects.all() + serializer_class = ConversationSerializer + + def list(self, request, *args, **kwargs): + cache_key = 'all_conversations' + cached_data = cache.get(cache_key) + if cached_data is None: + response = super().list(request, *args, **kwargs) + cache.set(cache_key, response.data, timeout=60*15) # Cache for 15 minutes + return Response(response.data) + return Response(cached_data) + + +logger = logging.getLogger(__name__) + +class FileUploadViewSet(viewsets.ModelViewSet): + queryset = FileUpload.objects.all() + serializer_class = FileUploadSerializer + permission_classes = [IsAdminOrReadOnly] + + def create(self, request, *args, **kwargs): + response = super().create(request, *args, **kwargs) + logger.info(f'File uploaded by {request.user} with filename {response.data["file"]}') + return response + + def destroy(self, request, *args, **kwargs): + file = self.get_object() + logger.info(f'File deleted by {request.user} with filename {file.file.name}') + return super().destroy(request, *args, **kwargs) + +class FileUploadViewSet(viewsets.ModelViewSet): + queryset = FileUpload.objects.all() + serializer_class = FileUploadSerializer + permission_classes = [IsAdminOrReadOnly] + + def create(self, request, *args, **kwargs): + # Your file upload logic + return super().create(request, *args, **kwargs) + +class ConversationViewSet(viewsets.ModelViewSet): + queryset = Conversation.objects.all() + serializer_class = ConversationSerializer + +class FileUploadViewSet(viewsets.ModelViewSet): + queryset = FileUpload.objects.all() + serializer_class = FileUploadSerializer + + def create(self, request, *args, **kwargs): + file = request.FILES.get('file') + if file: + # Handle file duplication and other custom logic here + # For example, check if a file with the same name already exists + if FileUpload.objects.filter(file=file.name).exists(): + return Response({'error': 'File with this name already exists'}, status=400) + return super().create(request, *args, **kwargs) @api_view(["GET"]) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 9de4f024a..7765357b3 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -19,6 +19,11 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +# Base directory of the project +BASE_DIR = Path(__file__).resolve().parent.parent +# Load environment variables from the .env file +load_dotenv(os.path.join(BASE_DIR, '.env')) + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ @@ -29,6 +34,8 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +DEBUG = os.getenv('DEBUG', 'False') == 'True' + ALLOWED_HOSTS = [] @@ -47,8 +54,22 @@ "authentication", "chat", "gpt", + + + ] +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', + ], +} + + MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -58,8 +79,13 @@ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "corsheaders.middleware.CorsMiddleware", + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', ] +CORS_ALLOW_ALL_ORIGINS = True + + ROOT_URLCONF = "backend.urls" TEMPLATES = [ @@ -88,6 +114,10 @@ "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", + 'USER': os.getenv('DATABASE_USER'), + 'PASSWORD': os.getenv('DATABASE_PASSWORD'), + 'HOST': 'localhost', + 'PORT': '5432', } } @@ -149,3 +179,25 @@ SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True CSRF_COOKIE_SAMESITE = "None" +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': 'django_debug.log', + }, + }, + 'root': { + 'handlers': ['file'], + 'level': 'DEBUG', + }, +} +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-snowflake', + } +} + diff --git a/backend/backend/urls.py b/backend/backend/urls.py index fa154c7fb..adc01eae0 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -17,4 +17,6 @@ def root_view(request): path("gpt/", include("gpt.urls")), path("auth/", include("authentication.urls")), path("", root_view), + path('nested_admin/', include('nested_admin.urls')), + ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/backend/tests.py b/backend/tests.py new file mode 100644 index 000000000..eb910b8b2 --- /dev/null +++ b/backend/tests.py @@ -0,0 +1,13 @@ +from django.test import TestCase +from .models import Conversation +from .utils import generate_summary + +class ConversationModelTest(TestCase): + def setUp(self): + self.conversation = Conversation.objects.create(content="Test content") + + def test_summary_generation(self): + # Assuming generate_summary sets a summary based on content + self.conversation.summary = generate_summary(self.conversation) + self.conversation.save() + self.assertEqual(self.conversation.summary, "Generated summary based on content")