This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

0
folders/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

37
folders/admin.py Normal file
View File

@@ -0,0 +1,37 @@
from django.contrib import admin
from .models import FolderStructure, FolderPermission, FolderBookmark, FolderActivity
@admin.register(FolderStructure)
class FolderStructureAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'parent', 'folder_type', 'is_system', 'is_shared', 'created_at')
list_filter = ('folder_type', 'is_system', 'is_shared', 'created_at')
search_fields = ('name', 'user__email')
raw_id_fields = ('user', 'parent')
date_hierarchy = 'created_at'
@admin.register(FolderPermission)
class FolderPermissionAdmin(admin.ModelAdmin):
list_display = ('folder', 'user', 'permission', 'granted_by', 'granted_at')
list_filter = ('permission', 'granted_at')
search_fields = ('folder__name', 'user__email', 'granted_by__email')
raw_id_fields = ('folder', 'user', 'granted_by')
@admin.register(FolderBookmark)
class FolderBookmarkAdmin(admin.ModelAdmin):
list_display = ('user', 'folder', 'created_at')
list_filter = ('created_at',)
search_fields = ('user__email', 'folder__name')
raw_id_fields = ('user', 'folder')
@admin.register(FolderActivity)
class FolderActivityAdmin(admin.ModelAdmin):
list_display = ('folder', 'user', 'activity_type', 'created_at')
list_filter = ('activity_type', 'created_at')
search_fields = ('folder__name', 'user__email', 'description')
raw_id_fields = ('folder', 'user')
readonly_fields = ('created_at',)
date_hierarchy = 'created_at'

6
folders/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class FoldersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'folders'

View File

@@ -0,0 +1,84 @@
# Generated by Django 4.2.7 on 2025-09-14 20:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='FolderStructure',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True)),
('folder_type', models.CharField(default='custom', max_length=20)),
('color', models.CharField(default='#007bff', max_length=7)),
('icon', models.CharField(default='folder', max_length=50)),
('is_system', models.BooleanField(default=False)),
('is_shared', models.BooleanField(default=False)),
('sort_order', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='folders.folderstructure')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_structures', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'folder_structures',
'ordering': ['sort_order', 'name'],
'unique_together': {('user', 'name', 'parent')},
},
),
migrations.CreateModel(
name='FolderActivity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('activity_type', models.CharField(choices=[('created', 'Created'), ('updated', 'Updated'), ('deleted', 'Deleted'), ('shared', 'Shared'), ('unshared', 'Unshared'), ('moved', 'Moved'), ('renamed', 'Renamed')], max_length=20)),
('description', models.TextField()),
('metadata', models.JSONField(default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='folders.folderstructure')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_activities', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'folder_activities',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='FolderPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('admin', 'Admin')], max_length=10)),
('granted_at', models.DateTimeField(auto_now_add=True)),
('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='folders.folderstructure')),
('granted_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='granted_permissions', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_permissions', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'folder_permissions',
'unique_together': {('folder', 'user')},
},
),
migrations.CreateModel(
name='FolderBookmark',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookmarks', to='folders.folderstructure')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_bookmarks', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'folder_bookmarks',
'unique_together': {('user', 'folder')},
},
),
]

View File

117
folders/models.py Normal file
View File

@@ -0,0 +1,117 @@
from django.db import models
from django.contrib.auth import get_user_model
from emails.models import EmailFolder
User = get_user_model()
class FolderStructure(models.Model):
"""Folder structure for organizing emails and other content."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='folder_structures')
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
folder_type = models.CharField(max_length=20, default='custom')
color = models.CharField(max_length=7, default='#007bff')
icon = models.CharField(max_length=50, default='folder')
is_system = models.BooleanField(default=False)
is_shared = models.BooleanField(default=False)
sort_order = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'folder_structures'
unique_together = ['user', 'name', 'parent']
ordering = ['sort_order', 'name']
def __str__(self):
return f"{self.user.email} - {self.name}"
def get_path(self):
"""Get full path of the folder."""
path = [self.name]
parent = self.parent
while parent:
path.insert(0, parent.name)
parent = parent.parent
return ' / '.join(path)
def get_children(self):
"""Get all children folders."""
return self.children.all().order_by('sort_order', 'name')
def get_descendants(self):
"""Get all descendant folders."""
descendants = []
for child in self.get_children():
descendants.append(child)
descendants.extend(child.get_descendants())
return descendants
class FolderPermission(models.Model):
"""Permissions for shared folders."""
PERMISSION_CHOICES = [
('read', 'Read'),
('write', 'Write'),
('admin', 'Admin'),
]
folder = models.ForeignKey(FolderStructure, on_delete=models.CASCADE, related_name='permissions')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='folder_permissions')
permission = models.CharField(max_length=10, choices=PERMISSION_CHOICES)
granted_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='granted_permissions')
granted_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'folder_permissions'
unique_together = ['folder', 'user']
def __str__(self):
return f"{self.folder.name} - {self.user.email} ({self.permission})"
class FolderBookmark(models.Model):
"""Bookmarks for quick access to folders."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='folder_bookmarks')
folder = models.ForeignKey(FolderStructure, on_delete=models.CASCADE, related_name='bookmarks')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'folder_bookmarks'
unique_together = ['user', 'folder']
def __str__(self):
return f"{self.user.email} - {self.folder.name}"
class FolderActivity(models.Model):
"""Activity log for folders."""
ACTIVITY_TYPES = [
('created', 'Created'),
('updated', 'Updated'),
('deleted', 'Deleted'),
('shared', 'Shared'),
('unshared', 'Unshared'),
('moved', 'Moved'),
('renamed', 'Renamed'),
]
folder = models.ForeignKey(FolderStructure, on_delete=models.CASCADE, related_name='activities')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='folder_activities')
activity_type = models.CharField(max_length=20, choices=ACTIVITY_TYPES)
description = models.TextField()
metadata = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'folder_activities'
ordering = ['-created_at']
def __str__(self):
return f"{self.folder.name} - {self.get_activity_type_display()} by {self.user.email}"

134
folders/serializers.py Normal file
View File

@@ -0,0 +1,134 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import FolderStructure, FolderPermission, FolderBookmark, FolderActivity
User = get_user_model()
class FolderStructureSerializer(serializers.ModelSerializer):
"""Serializer for folder structures."""
path = serializers.CharField(source='get_path', read_only=True)
children = serializers.SerializerMethodField()
email_count = serializers.SerializerMethodField()
unread_count = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField()
class Meta:
model = FolderStructure
fields = '__all__'
read_only_fields = ('user', 'created_at', 'updated_at')
def get_children(self, obj):
children = obj.get_children()
return FolderStructureSerializer(children, many=True, context=self.context).data
def get_email_count(self, obj):
# This would need to be implemented based on your email folder relationship
return 0
def get_unread_count(self, obj):
# This would need to be implemented based on your email folder relationship
return 0
def get_permissions(self, obj):
if obj.is_shared:
permissions = obj.permissions.all()
return FolderPermissionSerializer(permissions, many=True).data
return []
class FolderPermissionSerializer(serializers.ModelSerializer):
"""Serializer for folder permissions."""
user_name = serializers.CharField(source='user.get_full_name', read_only=True)
user_email = serializers.CharField(source='user.email', read_only=True)
granted_by_name = serializers.CharField(source='granted_by.get_full_name', read_only=True)
class Meta:
model = FolderPermission
fields = '__all__'
read_only_fields = ('granted_by', 'granted_at')
class FolderBookmarkSerializer(serializers.ModelSerializer):
"""Serializer for folder bookmarks."""
folder_name = serializers.CharField(source='folder.name', read_only=True)
folder_path = serializers.CharField(source='folder.get_path', read_only=True)
class Meta:
model = FolderBookmark
fields = '__all__'
read_only_fields = ('user', 'created_at')
class FolderActivitySerializer(serializers.ModelSerializer):
"""Serializer for folder activities."""
user_name = serializers.CharField(source='user.get_full_name', read_only=True)
user_email = serializers.CharField(source='user.email', read_only=True)
folder_name = serializers.CharField(source='folder.name', read_only=True)
class Meta:
model = FolderActivity
fields = '__all__'
read_only_fields = ('created_at',)
class FolderCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating folders."""
class Meta:
model = FolderStructure
fields = ('name', 'description', 'parent', 'folder_type', 'color', 'icon', 'sort_order')
def validate_name(self, value):
user = self.context['request'].user
parent = self.initial_data.get('parent')
# Check for duplicate names in the same parent folder
if FolderStructure.objects.filter(
user=user,
name=value,
parent_id=parent
).exists():
raise serializers.ValidationError("A folder with this name already exists in the same location.")
return value
class FolderShareSerializer(serializers.Serializer):
"""Serializer for sharing folders."""
user_email = serializers.EmailField()
permission = serializers.ChoiceField(choices=FolderPermission.PERMISSION_CHOICES)
def validate_user_email(self, value):
user = self.context['request'].user
if value == user.email:
raise serializers.ValidationError("You cannot share a folder with yourself.")
try:
User.objects.get(email=value)
except User.DoesNotExist:
raise serializers.ValidationError("User with this email does not exist.")
return value
class FolderMoveSerializer(serializers.Serializer):
"""Serializer for moving folders."""
new_parent = serializers.IntegerField(required=False, allow_null=True)
new_sort_order = serializers.IntegerField(required=False)
def validate_new_parent(self, value):
if value is not None:
user = self.context['request'].user
try:
folder = FolderStructure.objects.get(id=value, user=user)
return folder
except FolderStructure.DoesNotExist:
raise serializers.ValidationError("Parent folder does not exist or you don't have access to it.")
return None

21
folders/urls.py Normal file
View File

@@ -0,0 +1,21 @@
from django.urls import path
from . import views
urlpatterns = [
# Folders
path('', views.FolderListCreateView.as_view(), name='folder-list'),
path('tree/', views.FolderTreeView.as_view(), name='folder-tree'),
path('<int:pk>/', views.FolderDetailView.as_view(), name='folder-detail'),
path('<int:folder_id>/share/', views.share_folder, name='share-folder'),
path('<int:folder_id>/move/', views.move_folder, name='move-folder'),
path('<int:folder_id>/activities/', views.FolderActivityView.as_view(), name='folder-activities'),
path('stats/', views.folder_stats, name='folder-stats'),
# Folder permissions
path('<int:folder_id>/permissions/', views.FolderPermissionListCreateView.as_view(), name='folder-permission-list'),
path('<int:folder_id>/permissions/<int:pk>/', views.FolderPermissionDetailView.as_view(), name='folder-permission-detail'),
# Folder bookmarks
path('<int:folder_id>/bookmarks/', views.FolderBookmarkListCreateView.as_view(), name='folder-bookmark-list'),
path('bookmarks/<int:pk>/', views.FolderBookmarkDetailView.as_view(), name='folder-bookmark-detail'),
]

361
folders/views.py Normal file
View File

@@ -0,0 +1,361 @@
from rest_framework import generics, status, permissions
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.views import APIView
from django.shortcuts import get_object_or_404
from django.db.models import Q
from django.contrib.auth import get_user_model
from .models import FolderStructure, FolderPermission, FolderBookmark, FolderActivity
from .serializers import (
FolderStructureSerializer, FolderPermissionSerializer, FolderBookmarkSerializer,
FolderActivitySerializer, FolderCreateSerializer, FolderShareSerializer,
FolderMoveSerializer
)
User = get_user_model()
class FolderListCreateView(generics.ListCreateAPIView):
"""List and create folders."""
permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self):
if self.request.method == 'POST':
return FolderCreateSerializer
return FolderStructureSerializer
def get_queryset(self):
user = self.request.user
return FolderStructure.objects.filter(
Q(user=user) | Q(permissions__user=user)
).distinct().order_by('sort_order', 'name')
def perform_create(self, serializer):
folder = serializer.save(user=self.request.user)
# Log activity
FolderActivity.objects.create(
folder=folder,
user=self.request.user,
activity_type='created',
description=f"Created folder '{folder.name}'"
)
class FolderDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete folder."""
serializer_class = FolderStructureSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
return FolderStructure.objects.filter(
Q(user=user) | Q(permissions__user=user)
).distinct()
def perform_update(self, serializer):
folder = self.get_object()
old_name = folder.name
folder = serializer.save()
# Log activity
if old_name != folder.name:
FolderActivity.objects.create(
folder=folder,
user=self.request.user,
activity_type='renamed',
description=f"Renamed folder from '{old_name}' to '{folder.name}'"
)
else:
FolderActivity.objects.create(
folder=folder,
user=self.request.user,
activity_type='updated',
description=f"Updated folder '{folder.name}'"
)
def perform_destroy(self, instance):
# Log activity
FolderActivity.objects.create(
folder=instance,
user=self.request.user,
activity_type='deleted',
description=f"Deleted folder '{instance.name}'"
)
instance.delete()
class FolderTreeView(generics.ListAPIView):
"""Get folder tree structure."""
serializer_class = FolderStructureSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
return FolderStructure.objects.filter(
Q(user=user) | Q(permissions__user=user),
parent__isnull=True
).distinct().order_by('sort_order', 'name')
class FolderPermissionListCreateView(generics.ListCreateAPIView):
"""List and create folder permissions."""
serializer_class = FolderPermissionSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
folder_id = self.kwargs.get('folder_id')
folder = get_object_or_404(FolderStructure, id=folder_id)
# Check if user owns the folder or has admin permission
if folder.user != self.request.user:
permission = FolderPermission.objects.filter(
folder=folder,
user=self.request.user,
permission='admin'
).exists()
if not permission:
return FolderPermission.objects.none()
return FolderPermission.objects.filter(folder=folder)
def perform_create(self, serializer):
folder_id = self.kwargs.get('folder_id')
folder = get_object_or_404(FolderStructure, id=folder_id)
# Check if user owns the folder or has admin permission
if folder.user != self.request.user:
permission = FolderPermission.objects.filter(
folder=folder,
user=self.request.user,
permission='admin'
).exists()
if not permission:
raise PermissionError("You don't have permission to share this folder.")
# Get user to share with
user_email = serializer.validated_data['user_email']
user_to_share = get_object_or_404(User, email=user_email)
# Create permission
permission = FolderPermission.objects.create(
folder=folder,
user=user_to_share,
permission=serializer.validated_data['permission'],
granted_by=self.request.user
)
# Log activity
FolderActivity.objects.create(
folder=folder,
user=self.request.user,
activity_type='shared',
description=f"Shared folder '{folder.name}' with {user_email} ({permission.permission} permission)"
)
class FolderPermissionDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete folder permission."""
serializer_class = FolderPermissionSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
folder_id = self.kwargs.get('folder_id')
folder = get_object_or_404(FolderStructure, id=folder_id)
# Check if user owns the folder or has admin permission
if folder.user != self.request.user:
permission = FolderPermission.objects.filter(
folder=folder,
user=self.request.user,
permission='admin'
).exists()
if not permission:
return FolderPermission.objects.none()
return FolderPermission.objects.filter(folder=folder)
class FolderBookmarkListCreateView(generics.ListCreateAPIView):
"""List and create folder bookmarks."""
serializer_class = FolderBookmarkSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return FolderBookmark.objects.filter(user=self.request.user)
def perform_create(self, serializer):
folder_id = self.kwargs.get('folder_id')
folder = get_object_or_404(FolderStructure, id=folder_id)
# Check if user has access to the folder
if folder.user != self.request.user:
permission = FolderPermission.objects.filter(
folder=folder,
user=self.request.user
).exists()
if not permission:
raise PermissionError("You don't have access to this folder.")
serializer.save(user=self.request.user, folder=folder)
class FolderBookmarkDetailView(generics.RetrieveDestroyAPIView):
"""Retrieve or delete folder bookmark."""
serializer_class = FolderBookmarkSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return FolderBookmark.objects.filter(user=self.request.user)
class FolderActivityView(generics.ListAPIView):
"""List folder activities."""
serializer_class = FolderActivitySerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
folder_id = self.kwargs.get('folder_id')
folder = get_object_or_404(FolderStructure, id=folder_id)
# Check if user has access to the folder
if folder.user != self.request.user:
permission = FolderPermission.objects.filter(
folder=folder,
user=self.request.user
).exists()
if not permission:
return FolderActivity.objects.none()
return FolderActivity.objects.filter(folder=folder)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def share_folder(request, folder_id):
"""Share a folder with another user."""
folder = get_object_or_404(FolderStructure, id=folder_id)
# Check if user owns the folder
if folder.user != request.user:
return Response(
{'error': 'You can only share folders you own'},
status=status.HTTP_403_FORBIDDEN
)
serializer = FolderShareSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
user_email = serializer.validated_data['user_email']
permission = serializer.validated_data['permission']
try:
user_to_share = User.objects.get(email=user_email)
except User.DoesNotExist:
return Response(
{'error': 'User not found'},
status=status.HTTP_404_NOT_FOUND
)
# Create or update permission
permission_obj, created = FolderPermission.objects.get_or_create(
folder=folder,
user=user_to_share,
defaults={
'permission': permission,
'granted_by': request.user
}
)
if not created:
permission_obj.permission = permission
permission_obj.granted_by = request.user
permission_obj.save()
# Make folder shared
folder.is_shared = True
folder.save()
# Log activity
FolderActivity.objects.create(
folder=folder,
user=request.user,
activity_type='shared',
description=f"Shared folder '{folder.name}' with {user_email} ({permission} permission)"
)
return Response({
'message': f'Folder shared with {user_email}',
'permission': FolderPermissionSerializer(permission_obj).data
})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def move_folder(request, folder_id):
"""Move a folder to a new location."""
folder = get_object_or_404(FolderStructure, id=folder_id)
# Check if user owns the folder
if folder.user != request.user:
return Response(
{'error': 'You can only move folders you own'},
status=status.HTTP_403_FORBIDDEN
)
serializer = FolderMoveSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
new_parent = serializer.validated_data.get('new_parent')
new_sort_order = serializer.validated_data.get('new_sort_order')
old_parent = folder.parent
old_sort_order = folder.sort_order
# Update folder
folder.parent = new_parent
if new_sort_order is not None:
folder.sort_order = new_sort_order
folder.save()
# Log activity
FolderActivity.objects.create(
folder=folder,
user=request.user,
activity_type='moved',
description=f"Moved folder '{folder.name}' from {old_parent.name if old_parent else 'root'} to {new_parent.name if new_parent else 'root'}"
)
return Response({
'message': 'Folder moved successfully',
'folder': FolderStructureSerializer(folder).data
})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
def folder_stats(request):
"""Get folder statistics."""
user = request.user
stats = {
'total_folders': FolderStructure.objects.filter(user=user).count(),
'shared_folders': FolderStructure.objects.filter(user=user, is_shared=True).count(),
'bookmarked_folders': FolderBookmark.objects.filter(user=user).count(),
'recent_activities': FolderActivity.objects.filter(
Q(folder__user=user) | Q(folder__permissions__user=user)
).distinct().count(),
}
return Response(stats)