mirror of https://github.com/doccano/doccano.git
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1654 additions and 275 deletions
Split View
Diff Options
-
19backend/config/urls.py
-
6backend/groups/admin.py
-
41backend/groups/models.py
-
72backend/groups/serializers.py
-
29backend/groups/urls.py
-
109backend/groups/views.py
-
68backend/users/serializers.py
-
12backend/users/urls.py
-
98backend/users/views.py
-
12frontend/components/auth/FormRegister.vue
-
216frontend/components/groups/FormCreate.vue
-
39frontend/components/groups/FormDelete.vue
-
229frontend/components/groups/FormUpdate.vue
-
127frontend/components/groups/GroupList.vue
-
8frontend/components/layout/TheHeader.vue
-
39frontend/domain/models/group/group.ts
-
8frontend/domain/models/user/user.ts
-
3frontend/i18n/en/generic.js
-
20frontend/i18n/en/group.js
-
1frontend/i18n/en/header.js
-
4frontend/i18n/en/index.js
-
141frontend/pages/groups/_id/index.vue
-
161frontend/pages/groups/index.vue
-
208frontend/pages/users/_id.vue
-
7frontend/plugins/repositories.ts
-
5frontend/plugins/services.ts
-
8frontend/repositories/auth/apiAuthRepository.ts
-
134frontend/repositories/group/apiGroupRepository.ts
-
16frontend/repositories/user/apiUserRepository.ts
-
82frontend/services/application/group/GroupApplicationService.ts
-
1frontend/services/application/user/UserApplicationService.ts
-
6frontend/store/auth.js
@ -1,6 +0,0 @@ |
|||
from django.contrib import admin |
|||
|
|||
# Register your models here. |
|||
from .models import Group |
|||
|
|||
admin.site.register(Group) |
@ -1,41 +0,0 @@ |
|||
from django.db import models |
|||
|
|||
class Group(models.Model): |
|||
# Define fields exactly as they exist in auth_group |
|||
id = models.AutoField(primary_key=True) |
|||
name = models.CharField(max_length=150, unique=True) |
|||
|
|||
class Meta: |
|||
db_table = 'auth_group' # Explicitly map to existing table |
|||
managed = False # Prevent Django from modifying the table |
|||
|
|||
class GroupPermissions(models.Model): |
|||
# Define fields exactly as they exist in auth_group_permissions |
|||
id = models.AutoField(primary_key=True) |
|||
group = models.ForeignKey(Group, on_delete=models.CASCADE) |
|||
permission = models.ForeignKey('auth.Permission', on_delete=models.CASCADE) |
|||
|
|||
class Meta: |
|||
db_table = 'auth_group_permissions' # Explicitly map to existing table |
|||
managed = False # Prevent Django from modifying the table |
|||
|
|||
class Permission(models.Model): |
|||
# Define fields exactly as they exist in auth_permission |
|||
id = models.AutoField(primary_key=True) |
|||
name = models.CharField(max_length=255) |
|||
content_type = models.ForeignKey('ContentType', on_delete=models.CASCADE) |
|||
codename = models.CharField(max_length=100) |
|||
|
|||
class Meta: |
|||
db_table = 'auth_permission' # Explicitly map to existing table |
|||
managed = False # Prevent Django from modifying the table |
|||
|
|||
class ContentType(models.Model): |
|||
# Define fields exactly as they exist in django_content_type |
|||
id = models.AutoField(primary_key=True) |
|||
app_label = models.CharField(max_length=100) |
|||
model = models.CharField(max_length=100) |
|||
|
|||
class Meta: |
|||
db_table = 'django_content_type' # Explicitly map to existing table |
|||
managed = False # Prevent Django from modifying the table |
@ -1,54 +1,42 @@ |
|||
from rest_framework import serializers |
|||
from .models import Group, GroupPermissions, Permission |
|||
from django.contrib.auth.models import Group, Permission, ContentType |
|||
|
|||
class GroupSerializer(serializers.ModelSerializer): |
|||
permission_names = serializers.SerializerMethodField() |
|||
permissions = serializers.PrimaryKeyRelatedField( |
|||
many=True, |
|||
queryset=Permission.objects.all(), |
|||
required=False |
|||
) |
|||
|
|||
class Meta: |
|||
model = Group |
|||
fields = ['id', 'name'] # Include only the fields you need |
|||
|
|||
class GroupCreateSerializer(serializers.ModelSerializer): |
|||
class Meta: |
|||
model = Group |
|||
fields = ['name'] |
|||
extra_kwargs = { |
|||
'name': {'required': True} |
|||
} |
|||
def create(self, validated_data): |
|||
group = Group(**validated_data) |
|||
group.save() |
|||
return group |
|||
def update(self, instance, validated_data): |
|||
instance.name = validated_data.get('name', instance.name) |
|||
instance.save() |
|||
return instance |
|||
def delete(self, instance): |
|||
instance.delete() |
|||
return instance |
|||
def validate(self, data): |
|||
if 'name' in data and not data['name']: |
|||
raise serializers.ValidationError("Group name cannot be empty.") |
|||
return data |
|||
|
|||
class GroupPermissionsSerializer(serializers.ModelSerializer): |
|||
class Meta: |
|||
model = GroupPermissions |
|||
fields = ['id', 'group_id', 'permission_id'] # Include only the fields you need |
|||
fields = ['id', 'name', 'permissions', 'permission_names'] |
|||
|
|||
def get_permission_names(self, obj): |
|||
# Create a dictionary mapping permission IDs to their display names |
|||
permissions_dict = {} |
|||
for permission in Permission.objects.filter(group=obj): |
|||
permissions_dict[permission.id] = { |
|||
'name': permission.name, |
|||
'codename': permission.codename, |
|||
'content_type': permission.content_type.app_label + '.' + permission.content_type.model |
|||
} |
|||
return permissions_dict |
|||
|
|||
class PermissionSerializer(serializers.ModelSerializer): |
|||
label = serializers.SerializerMethodField() |
|||
|
|||
class Meta: |
|||
model = Permission |
|||
fields = ['id', 'name', 'content_type_id', 'codename'] # Include only the fields you need |
|||
extra_kwargs = { |
|||
'name': {'required': True}, |
|||
'content_type_id': {'required': True}, |
|||
'codename': {'required': True} |
|||
} |
|||
fields = '__all__' |
|||
|
|||
def get_label(self, obj): |
|||
if obj.content_type: |
|||
return f"{obj.content_type.app_label} | {obj.content_type.model} | {obj.name}" |
|||
return obj.name |
|||
|
|||
class ContentTypeSerializer(serializers.ModelSerializer): |
|||
class Meta: |
|||
model = 'ContentType' |
|||
fields = ['id', 'app_label', 'model'] # Include only the fields you need |
|||
extra_kwargs = { |
|||
'app_label': {'required': True}, |
|||
'model': {'required': True} |
|||
} |
|||
model = ContentType |
|||
fields = '__all__' |
@ -1,23 +1,12 @@ |
|||
from django.urls import path |
|||
from .views import ( |
|||
Groups, GroupDetail, GroupCreate, |
|||
GroupPermissionsList, GroupPermissionsCreate, GroupPermissionsDetail, |
|||
PermissionList, PermissionCreate, PermissionDetail |
|||
) |
|||
from django.urls import path, include |
|||
from rest_framework.routers import DefaultRouter |
|||
from .views import GroupViewSet, PermissionViewSet, ContentTypeViewSet |
|||
|
|||
router = DefaultRouter() |
|||
router.register(r'groups', GroupViewSet) |
|||
router.register(r'permissions', PermissionViewSet) |
|||
router.register(r'content-types', ContentTypeViewSet) |
|||
|
|||
urlpatterns = [ |
|||
# Group URLs |
|||
path(route="groups/<int:id>", view=GroupDetail.as_view(), name="group_detail"), |
|||
path(route="groups/create/", view=GroupCreate.as_view(), name="group_create"), |
|||
path(route="groups", view=Groups.as_view(), name="group_list"), |
|||
|
|||
# GroupPermissions URLs |
|||
path(route="group-permissions/<int:id>", view=GroupPermissionsDetail.as_view(), name="group_permissions_detail"), |
|||
path(route="group-permissions/create/", view=GroupPermissionsCreate.as_view(), name="group_permissions_create"), |
|||
path(route="group-permissions", view=GroupPermissionsList.as_view(), name="group_permissions_list"), |
|||
|
|||
# Permission URLs |
|||
path(route="permissions/<int:id>", view=PermissionDetail.as_view(), name="permission_detail"), |
|||
path(route="permissions/create/", view=PermissionCreate.as_view(), name="permission_create"), |
|||
path(route="permissions", view=PermissionList.as_view(), name="permission_list"), |
|||
path('', include(router.urls)), |
|||
] |
@ -1,66 +1,51 @@ |
|||
from django.shortcuts import render |
|||
from django_filters.rest_framework import DjangoFilterBackend |
|||
from rest_framework import filters, generics |
|||
from rest_framework.permissions import IsAuthenticated |
|||
from .models import Group, GroupPermissions, Permission |
|||
from .serializers import GroupSerializer, GroupCreateSerializer, GroupPermissionsSerializer, PermissionSerializer |
|||
from projects.permissions import IsProjectAdmin |
|||
|
|||
class Groups(generics.ListAPIView): |
|||
queryset = Group.objects.all() |
|||
serializer_class = GroupSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) |
|||
search_fields = ("name",) |
|||
ordering_fields = ("id", "name") |
|||
ordering = ("name",) # Default ordering |
|||
|
|||
class GroupCreate(generics.CreateAPIView): |
|||
queryset = Group.objects.all() |
|||
serializer_class = GroupCreateSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
|
|||
class GroupDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
from rest_framework import viewsets, permissions, filters |
|||
from django.contrib.auth.models import Group, Permission, ContentType |
|||
from .serializers import GroupSerializer, PermissionSerializer, ContentTypeSerializer |
|||
|
|||
class AdminWritePermission(permissions.BasePermission): |
|||
""" |
|||
Custom permission to allow only admins to create, update or delete objects. |
|||
All authenticated users can read. |
|||
""" |
|||
def has_permission(self, request, view): |
|||
# Allow read-only for authenticated users |
|||
if request.method in permissions.SAFE_METHODS: |
|||
return request.user and request.user.is_authenticated |
|||
# For write operations, require admin status |
|||
return request.user and request.user.is_staff |
|||
|
|||
class GroupViewSet(viewsets.ModelViewSet): |
|||
""" |
|||
API endpoints for managing user groups |
|||
""" |
|||
queryset = Group.objects.all() |
|||
serializer_class = GroupSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
lookup_field = 'id' |
|||
|
|||
class GroupPermissionsList(generics.ListAPIView): |
|||
queryset = GroupPermissions.objects.all() |
|||
serializer_class = GroupPermissionsSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) |
|||
filterset_fields = ('group_id', 'permission_id') |
|||
ordering_fields = ('id',) |
|||
|
|||
class GroupPermissionsCreate(generics.CreateAPIView): |
|||
queryset = GroupPermissions.objects.all() |
|||
serializer_class = GroupPermissionsSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
|
|||
class GroupPermissionsDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = GroupPermissions.objects.all() |
|||
serializer_class = GroupPermissionsSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
lookup_field = 'id' |
|||
|
|||
class PermissionList(generics.ListAPIView): |
|||
queryset = Permission.objects.all() |
|||
serializer_class = PermissionSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) |
|||
search_fields = ('name', 'codename') |
|||
ordering_fields = ('id', 'name', 'codename') |
|||
ordering = ('name',) |
|||
|
|||
class PermissionCreate(generics.CreateAPIView): |
|||
queryset = Permission.objects.all() |
|||
serializer_class = PermissionSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
|
|||
class PermissionDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
permission_classes = [AdminWritePermission] |
|||
swagger_tags = ['Groups'] |
|||
filter_backends = [filters.OrderingFilter] |
|||
ordering_fields = ['id', 'name'] |
|||
ordering = ['name'] |
|||
|
|||
class PermissionViewSet(viewsets.ModelViewSet): |
|||
""" |
|||
API endpoints for viewing permissions |
|||
""" |
|||
queryset = Permission.objects.all() |
|||
serializer_class = PermissionSerializer |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
lookup_field = 'id' |
|||
permission_classes = [AdminWritePermission] |
|||
swagger_tags = ['Permissions'] |
|||
filter_backends = [filters.OrderingFilter] |
|||
ordering_fields = ['id', 'name', 'codename', 'content_type'] |
|||
ordering = ['content_type', 'codename'] |
|||
|
|||
class ContentTypeViewSet(viewsets.ModelViewSet): |
|||
""" |
|||
API endpoints for viewing content types |
|||
""" |
|||
queryset = ContentType.objects.all() |
|||
serializer_class = ContentTypeSerializer |
|||
permission_classes = [AdminWritePermission] |
|||
swagger_tags = ['Content Types'] |
|||
filter_backends = [filters.OrderingFilter] |
|||
ordering_fields = ['id', 'app_label', 'model'] |
|||
ordering = ['app_label', 'model'] |
@ -1,15 +1,75 @@ |
|||
from django.contrib.auth import get_user_model |
|||
from rest_framework import serializers |
|||
from django.contrib.auth.models import Group |
|||
from django.contrib.auth.password_validation import validate_password |
|||
|
|||
User = get_user_model() |
|||
|
|||
class UserSerializer(serializers.ModelSerializer): |
|||
groups_details = serializers.SerializerMethodField() |
|||
|
|||
class Meta: |
|||
model = get_user_model() |
|||
fields = ("id", "username", "email", "is_staff", "is_superuser", "is_active") |
|||
model = User |
|||
fields = ("id", "username", "email", "is_staff", "is_superuser", "is_active", "groups", "groups_details") |
|||
|
|||
def get_groups_details(self, obj): |
|||
groups_dict = {} |
|||
for group in obj.groups.all(): |
|||
groups_dict[group.id] = { |
|||
'name': group.name |
|||
} |
|||
return groups_dict |
|||
|
|||
|
|||
class UserDetailSerializer(serializers.ModelSerializer): |
|||
groups = serializers.PrimaryKeyRelatedField( |
|||
many=True, |
|||
queryset=Group.objects.all(), |
|||
required=False |
|||
) |
|||
groups_details = serializers.SerializerMethodField() |
|||
|
|||
class Meta: |
|||
model = get_user_model() |
|||
model = User |
|||
fields = ("id", "username", "email", "is_staff", "is_superuser", "is_active", |
|||
"first_name", "last_name", "date_joined") |
|||
"first_name", "last_name", "date_joined", "groups", "groups_details") |
|||
|
|||
def get_groups_details(self, obj): |
|||
groups_dict = {} |
|||
for group in obj.groups.all(): |
|||
groups_dict[group.id] = { |
|||
'name': group.name |
|||
} |
|||
return groups_dict |
|||
|
|||
|
|||
class RegisterSerializer(serializers.ModelSerializer): |
|||
email = serializers.EmailField(required=True) |
|||
password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) |
|||
password_confirm = serializers.CharField(write_only=True, required=True) |
|||
|
|||
class Meta: |
|||
model = User |
|||
fields = ('username', 'email', 'password', 'password_confirm') |
|||
|
|||
def validate(self, attrs): |
|||
if attrs['password'] != attrs['password_confirm']: |
|||
raise serializers.ValidationError({"password_confirm": "Password fields didn't match."}) |
|||
|
|||
# Check if email already exists |
|||
if User.objects.filter(email=attrs['email']).exists(): |
|||
raise serializers.ValidationError({"email": "User with this email already exists."}) |
|||
|
|||
return attrs |
|||
|
|||
def create(self, validated_data): |
|||
# Remove password_confirm as we don't need it to create the user |
|||
validated_data.pop('password_confirm') |
|||
|
|||
user = User.objects.create_user( |
|||
username=validated_data['username'], |
|||
email=validated_data['email'], |
|||
password=validated_data['password'] |
|||
) |
|||
|
|||
return user |
@ -1,11 +1,13 @@ |
|||
from django.urls import include, path |
|||
from rest_framework.routers import DefaultRouter |
|||
from .views import UserViewSet, MeView, RegisterView |
|||
|
|||
from .views import Me, UserCreation, Users, UserDetail |
|||
router = DefaultRouter() |
|||
router.register(r"users", UserViewSet) |
|||
|
|||
urlpatterns = [ |
|||
path(route="me", view=Me.as_view(), name="me"), |
|||
path(route="users/<int:user_id>", view=UserDetail.as_view(), name="user_detail"), |
|||
path(route="users", view=Users.as_view(), name="user_list"), |
|||
path(route="users/create/", view=UserCreation.as_view(), name="user_create"), |
|||
path("me", MeView.as_view(), name="me"), |
|||
path("register", RegisterView.as_view(), name="register"), |
|||
path("", include(router.urls)), |
|||
path("auth/", include("dj_rest_auth.urls")), |
|||
] |
@ -0,0 +1,216 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-card-title>{{ $t('group.create') }}</v-card-title> |
|||
<v-card-text> |
|||
<v-container> |
|||
<v-form ref="form" v-model="valid"> |
|||
<v-text-field |
|||
v-model="name" |
|||
:label="$t('group.name')" |
|||
:rules="nameRules" |
|||
required |
|||
/> |
|||
<v-card-subtitle>{{ $t('group.permissions') }}</v-card-subtitle> |
|||
<v-progress-circular |
|||
v-if="isLoadingPermissions" |
|||
indeterminate |
|||
color="primary" |
|||
class="my-5" |
|||
></v-progress-circular> |
|||
<template v-else> |
|||
<v-autocomplete |
|||
v-model="selectedPermissions" |
|||
:items="permissions" |
|||
:item-text="permissionLabel" |
|||
:search-input.sync="permissionsSearch" |
|||
:label="$t('group.selectPermissions')" |
|||
:no-data-text="$t('vuetify.noDataAvailable')" |
|||
chips |
|||
small-chips |
|||
deletable-chips |
|||
multiple |
|||
clearable |
|||
dense |
|||
outlined |
|||
hide-selected |
|||
return-object |
|||
> |
|||
<template #selection="{ item, index }"> |
|||
<v-chip |
|||
v-if="index === 0" |
|||
small |
|||
close |
|||
@click:close="removePermission(item)" |
|||
> |
|||
<span>{{ permissionLabel(item) }}</span> |
|||
</v-chip> |
|||
<span v-if="index === 1" class="grey--text text-caption"> |
|||
(+{{ selectedPermissions.length - 1 }} {{ $t('generic.more') }}) |
|||
</span> |
|||
</template> |
|||
<template #item="{ item }"> |
|||
<v-list-item-content> |
|||
<v-list-item-title> |
|||
{{ permissionLabel(item) }} |
|||
</v-list-item-title> |
|||
<v-list-item-subtitle> |
|||
{{ item.codename }} |
|||
</v-list-item-subtitle> |
|||
</v-list-item-content> |
|||
</template> |
|||
</v-autocomplete> |
|||
|
|||
<div v-if="selectedPermissions && selectedPermissions.length > 0" class="mt-4"> |
|||
<div class="subtitle-1 mb-2"> |
|||
{{ $t('group.selectedPermissions') }} ({{ selectedPermissions.length }}) |
|||
</div> |
|||
<div class="selected-permissions-container"> |
|||
<v-chip-group column> |
|||
<v-chip |
|||
v-for="permission in selectedPermissions" |
|||
:key="permission.id" |
|||
small |
|||
close |
|||
class="ma-1" |
|||
@click:close="removePermission(permission)" |
|||
> |
|||
{{ permissionLabel(permission) }} |
|||
</v-chip> |
|||
</v-chip-group> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</v-form> |
|||
</v-container> |
|||
</v-card-text> |
|||
<v-divider></v-divider> |
|||
<v-card-actions> |
|||
<v-spacer></v-spacer> |
|||
<v-btn text color="grey" @click="$emit('cancel')"> |
|||
{{ $t('generic.cancel') }} |
|||
</v-btn> |
|||
<v-btn |
|||
text |
|||
color="primary" |
|||
:disabled="!valid || isSubmitting" |
|||
@click="create" |
|||
> |
|||
{{ $t('generic.create') }} |
|||
</v-btn> |
|||
</v-card-actions> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import { Permission } from '@/domain/models/group/group' |
|||
|
|||
export default Vue.extend({ |
|||
data() { |
|||
return { |
|||
valid: false, |
|||
name: '', |
|||
nameRules: [ |
|||
(v: string) => !!v || this.$t('rules.required') |
|||
], |
|||
isSubmitting: false, |
|||
permissions: [] as Permission[], |
|||
selectedPermissions: [] as Permission[], |
|||
isLoadingPermissions: false, |
|||
permissionsSearch: '', |
|||
filteredPermissions: [] as Permission[] |
|||
} |
|||
}, |
|||
|
|||
watch: { |
|||
permissionsSearch(_val) { |
|||
// When using v-autocomplete with server-side filtering, |
|||
// we could implement additional filtering here if needed |
|||
} |
|||
}, |
|||
|
|||
created() { |
|||
// Ensure selectedPermissions is initialized as an empty array |
|||
this.selectedPermissions = [] |
|||
}, |
|||
|
|||
async mounted() { |
|||
await this.fetchPermissions() |
|||
}, |
|||
|
|||
methods: { |
|||
permissionLabel(item: Permission): string { |
|||
return item && (item.label || item.name) || '' |
|||
}, |
|||
|
|||
removePermission(permission: Permission) { |
|||
if (!this.selectedPermissions) { |
|||
this.selectedPermissions = [] |
|||
return |
|||
} |
|||
this.selectedPermissions = this.selectedPermissions.filter(p => p.id !== permission.id) |
|||
}, |
|||
|
|||
async fetchPermissions() { |
|||
this.isLoadingPermissions = true |
|||
try { |
|||
// Fetch all permissions - could be paginated if there are too many |
|||
const response = await this.$services.group.listPermissions('limit=1000') |
|||
this.permissions = response.results |
|||
} catch (error) { |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'error', |
|||
text: 'Failed to load permissions' |
|||
}) |
|||
} finally { |
|||
this.isLoadingPermissions = false |
|||
} |
|||
}, |
|||
|
|||
async create() { |
|||
if (!(this.$refs.form as any).validate()) { |
|||
return |
|||
} |
|||
this.isSubmitting = true |
|||
try { |
|||
// Ensure we always have an array of permission IDs |
|||
const permissionIds = Array.isArray(this.selectedPermissions) |
|||
? this.selectedPermissions.map(p => p.id) |
|||
: []; |
|||
|
|||
// Log for debugging |
|||
console.log('Sending permissions:', permissionIds) |
|||
|
|||
await this.$services.group.createGroup({ |
|||
name: this.name, |
|||
permissions: permissionIds |
|||
}) |
|||
this.$emit('created') |
|||
} catch (error) { |
|||
console.error('Create group error:', error) |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'error', |
|||
text: 'Failed to create group' |
|||
}) |
|||
} finally { |
|||
this.isSubmitting = false |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.v-autocomplete { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.selected-permissions-container { |
|||
max-height: 200px; |
|||
overflow-y: auto; |
|||
border: 1px solid #e0e0e0; |
|||
border-radius: 4px; |
|||
padding: 8px; |
|||
background-color: #fafafa; |
|||
} |
|||
</style> |
@ -0,0 +1,39 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-card-title class="headline">{{ $t('generic.deleteConfirm') }}</v-card-title> |
|||
<v-card-text> |
|||
{{ $tc('group.confirmDelete', selected.length, { count: selected.length }) }} |
|||
<v-list dense> |
|||
<v-list-item v-for="group in selected" :key="group.id"> |
|||
<v-list-item-content> |
|||
<v-list-item-title>{{ group.name }}</v-list-item-title> |
|||
</v-list-item-content> |
|||
</v-list-item> |
|||
</v-list> |
|||
</v-card-text> |
|||
<v-card-actions> |
|||
<v-spacer></v-spacer> |
|||
<v-btn color="grey darken-1" text @click="$emit('cancel')"> |
|||
{{ $t('generic.cancel') }} |
|||
</v-btn> |
|||
<v-btn color="error" text @click="$emit('remove')"> |
|||
{{ $t('generic.delete') }} |
|||
</v-btn> |
|||
</v-card-actions> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import type { PropType } from 'vue' |
|||
import { Group } from '@/domain/models/group/group' |
|||
|
|||
export default Vue.extend({ |
|||
props: { |
|||
selected: { |
|||
type: Array as PropType<Group[]>, |
|||
required: true |
|||
} |
|||
} |
|||
}) |
|||
</script> |
@ -0,0 +1,229 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-card-title>{{ $t('group.edit') }}</v-card-title> |
|||
<v-card-text> |
|||
<v-container> |
|||
<v-form ref="form" v-model="valid"> |
|||
<v-text-field |
|||
v-model="name" |
|||
:label="$t('group.name')" |
|||
:rules="nameRules" |
|||
required |
|||
/> |
|||
<v-card-subtitle>{{ $t('group.permissions') }}</v-card-subtitle> |
|||
<v-progress-circular |
|||
v-if="isLoadingPermissions" |
|||
indeterminate |
|||
color="primary" |
|||
class="my-5" |
|||
></v-progress-circular> |
|||
<template v-else> |
|||
<v-autocomplete |
|||
v-model="selectedPermissions" |
|||
:items="permissions" |
|||
:item-text="permissionLabel" |
|||
:search-input.sync="permissionsSearch" |
|||
:label="$t('group.selectPermissions')" |
|||
:no-data-text="$t('vuetify.noDataAvailable')" |
|||
chips |
|||
small-chips |
|||
deletable-chips |
|||
multiple |
|||
clearable |
|||
dense |
|||
outlined |
|||
hide-selected |
|||
return-object |
|||
> |
|||
<template #selection="{ item, index }"> |
|||
<v-chip |
|||
v-if="index === 0" |
|||
small |
|||
close |
|||
@click:close="removePermission(item)" |
|||
> |
|||
<span>{{ permissionLabel(item) }}</span> |
|||
</v-chip> |
|||
<span v-if="index === 1" class="grey--text text-caption"> |
|||
(+{{ selectedPermissions.length - 1 }} {{ $t('generic.more') }}) |
|||
</span> |
|||
</template> |
|||
<template #item="{ item }"> |
|||
<v-list-item-content> |
|||
<v-list-item-title> |
|||
{{ permissionLabel(item) }} |
|||
</v-list-item-title> |
|||
<v-list-item-subtitle> |
|||
{{ item.codename }} |
|||
</v-list-item-subtitle> |
|||
</v-list-item-content> |
|||
</template> |
|||
</v-autocomplete> |
|||
|
|||
<div v-if="selectedPermissions && selectedPermissions.length > 0" class="mt-4"> |
|||
<div class="subtitle-1 mb-2"> |
|||
{{ $t('group.selectedPermissions') }} ({{ selectedPermissions.length }}) |
|||
</div> |
|||
<div class="selected-permissions-container"> |
|||
<v-chip-group column> |
|||
<v-chip |
|||
v-for="permission in selectedPermissions" |
|||
:key="permission.id" |
|||
small |
|||
close |
|||
class="ma-1" |
|||
@click:close="removePermission(permission)" |
|||
> |
|||
{{ permissionLabel(permission) }} |
|||
</v-chip> |
|||
</v-chip-group> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</v-form> |
|||
</v-container> |
|||
</v-card-text> |
|||
<v-divider></v-divider> |
|||
<v-card-actions> |
|||
<v-spacer></v-spacer> |
|||
<v-btn text color="grey" @click="$emit('cancel')"> |
|||
{{ $t('generic.cancel') }} |
|||
</v-btn> |
|||
<v-btn |
|||
text |
|||
color="primary" |
|||
:disabled="!valid || isSubmitting" |
|||
@click="update" |
|||
> |
|||
{{ $t('generic.save') }} |
|||
</v-btn> |
|||
</v-card-actions> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import { GroupDetails, Permission } from '@/domain/models/group/group' |
|||
|
|||
export default Vue.extend({ |
|||
props: { |
|||
group: { |
|||
type: Object as () => GroupDetails, |
|||
required: true |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
valid: false, |
|||
name: this.group.name, |
|||
nameRules: [ |
|||
(v: string) => !!v || this.$t('rules.required') |
|||
], |
|||
isSubmitting: false, |
|||
permissions: [] as Permission[], |
|||
selectedPermissions: [] as Permission[], |
|||
isLoadingPermissions: false, |
|||
permissionsSearch: '' |
|||
} |
|||
}, |
|||
|
|||
created() { |
|||
// Ensure selectedPermissions is initialized as an empty array |
|||
this.selectedPermissions = [] |
|||
}, |
|||
|
|||
watch: { |
|||
permissionsSearch(_val) { |
|||
// When using v-autocomplete with server-side filtering, |
|||
// we could implement additional filtering here if needed |
|||
} |
|||
}, |
|||
|
|||
async mounted() { |
|||
await this.fetchPermissions() |
|||
}, |
|||
|
|||
methods: { |
|||
permissionLabel(item: Permission): string { |
|||
return item && (item.label || item.name) || '' |
|||
}, |
|||
|
|||
removePermission(permission: Permission) { |
|||
if (!this.selectedPermissions) { |
|||
this.selectedPermissions = [] |
|||
return |
|||
} |
|||
this.selectedPermissions = this.selectedPermissions.filter(p => p.id !== permission.id) |
|||
}, |
|||
|
|||
async fetchPermissions() { |
|||
this.isLoadingPermissions = true |
|||
try { |
|||
// Fetch all permissions - could be paginated if there are too many |
|||
const response = await this.$services.group.listPermissions('limit=1000') |
|||
this.permissions = response.results |
|||
|
|||
// Select permissions that belong to this group |
|||
if (this.group.permissions && this.group.permissions.length > 0) { |
|||
this.selectedPermissions = this.permissions.filter(p => |
|||
this.group.permissions?.includes(p.id) |
|||
) |
|||
} |
|||
} catch (error) { |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'error', |
|||
text: 'Failed to load permissions' |
|||
}) |
|||
} finally { |
|||
this.isLoadingPermissions = false |
|||
} |
|||
}, |
|||
|
|||
async update() { |
|||
if (!(this.$refs.form as any).validate()) { |
|||
return |
|||
} |
|||
this.isSubmitting = true |
|||
try { |
|||
// Ensure we always have an array of permission IDs |
|||
const permissionIds = Array.isArray(this.selectedPermissions) |
|||
? this.selectedPermissions.map(p => p.id) |
|||
: []; |
|||
|
|||
// Log for debugging |
|||
console.log('Updating with permissions:', permissionIds); |
|||
|
|||
await this.$services.group.updateGroup(this.group.id, { |
|||
name: this.name, |
|||
permissions: permissionIds |
|||
}) |
|||
this.$emit('updated') |
|||
} catch (error) { |
|||
console.error('Update group error:', error) |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'error', |
|||
text: 'Failed to update group' |
|||
}) |
|||
} finally { |
|||
this.isSubmitting = false |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.v-autocomplete { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.selected-permissions-container { |
|||
max-height: 200px; |
|||
overflow-y: auto; |
|||
border: 1px solid #e0e0e0; |
|||
border-radius: 4px; |
|||
padding: 8px; |
|||
background-color: #fafafa; |
|||
} |
|||
</style> |
@ -0,0 +1,127 @@ |
|||
<template> |
|||
<v-data-table |
|||
:value="value" |
|||
:headers="headers" |
|||
:items="items" |
|||
:options.sync="options" |
|||
:server-items-length="total" |
|||
:search="search" |
|||
:loading="isLoading" |
|||
:loading-text="$t('generic.loading')" |
|||
:no-data-text="$t('vuetify.noDataAvailable')" |
|||
:footer-props="{ |
|||
showFirstLastPage: true, |
|||
'items-per-page-options': [10, 50, 100], |
|||
'items-per-page-text': $t('vuetify.itemsPerPageText'), |
|||
'page-text': $t('dataset.pageText') |
|||
}" |
|||
item-key="id" |
|||
show-select |
|||
@input="$emit('input', $event)" |
|||
> |
|||
<template #top> |
|||
<v-text-field |
|||
v-model="search" |
|||
:prepend-inner-icon="mdiMagnify" |
|||
:label="$t('generic.search')" |
|||
single-line |
|||
hide-details |
|||
filled |
|||
/> |
|||
</template> |
|||
<template #[`item.name`]="{ item }"> |
|||
<nuxt-link :to="localePath(`/groups/${item.id}`)"> |
|||
<span>{{ item.name }}</span> |
|||
</nuxt-link> |
|||
</template> |
|||
</v-data-table> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { mdiMagnify } from '@mdi/js' |
|||
import type { PropType } from 'vue' |
|||
import Vue from 'vue' |
|||
import { DataOptions } from 'vuetify/types' |
|||
import { Group } from '~/domain/models/group/group' |
|||
|
|||
export default Vue.extend({ |
|||
props: { |
|||
isLoading: { |
|||
type: Boolean, |
|||
default: false, |
|||
required: true |
|||
}, |
|||
items: { |
|||
type: Array as PropType<Group[]>, |
|||
default: () => [], |
|||
required: true |
|||
}, |
|||
value: { |
|||
type: Array as PropType<Group[]>, |
|||
default: () => [], |
|||
required: true |
|||
}, |
|||
total: { |
|||
type: Number, |
|||
default: 0, |
|||
required: true |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
search: this.$route.query.q, |
|||
options: {} as DataOptions, |
|||
mdiMagnify |
|||
} |
|||
}, |
|||
|
|||
computed: { |
|||
headers(): { text: any; value: string; sortable?: boolean; align?: string }[] { |
|||
return [ |
|||
{ text: this.$t('group.name'), value: 'name' }, |
|||
{ text: this.$t('group.id'), value: 'id' } |
|||
] |
|||
} |
|||
}, |
|||
|
|||
watch: { |
|||
options: { |
|||
handler() { |
|||
this.updateQuery({ |
|||
query: { |
|||
limit: this.options.itemsPerPage?.toString(), |
|||
offset: ((this.options.page ?? 1) - 1) * (this.options.itemsPerPage ?? 10), |
|||
q: this.search |
|||
} |
|||
}) |
|||
}, |
|||
deep: true |
|||
}, |
|||
search() { |
|||
this.updateQuery({ |
|||
query: { |
|||
limit: this.options.itemsPerPage?.toString(), |
|||
offset: '0', |
|||
q: this.search |
|||
} |
|||
}) |
|||
this.options.page = 1 |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
updateQuery(payload: any) { |
|||
const { sortBy, sortDesc } = this.options |
|||
if (sortBy && sortDesc && sortBy.length === 1 && sortDesc.length === 1) { |
|||
payload.query.ordering = sortBy[0] |
|||
payload.query.orderBy = sortDesc[0] ? '-' : '' |
|||
} else { |
|||
payload.query.ordering = 'name' |
|||
payload.query.orderBy = '' |
|||
} |
|||
this.$emit('update:query', payload) |
|||
} |
|||
} |
|||
}) |
|||
</script> |
@ -0,0 +1,39 @@ |
|||
export class Group { |
|||
constructor( |
|||
public id: number, |
|||
public name: string |
|||
) {} |
|||
} |
|||
|
|||
export class GroupDetails extends Group { |
|||
constructor( |
|||
public id: number, |
|||
public name: string, |
|||
public permissions?: number[], |
|||
public permission_names?: {[key: string]: { |
|||
name: string; |
|||
codename: string; |
|||
content_type: string; |
|||
}} |
|||
) { |
|||
super(id, name); |
|||
} |
|||
} |
|||
|
|||
export class Permission { |
|||
constructor( |
|||
public id: number, |
|||
public name: string, |
|||
public codename: string, |
|||
public content_type?: number, |
|||
public label?: string |
|||
) {} |
|||
} |
|||
|
|||
export class ContentType { |
|||
constructor( |
|||
public id: number, |
|||
public app_label: string, |
|||
public model: string |
|||
) {} |
|||
} |
@ -0,0 +1,20 @@ |
|||
export default { |
|||
create: 'Create Group', |
|||
edit: 'Edit Group', |
|||
id: 'ID', |
|||
name: 'Name', |
|||
details: 'Group Details', |
|||
permissions: 'Permissions', |
|||
permissionName: 'Permission Name', |
|||
permissionCode: 'Permission Code', |
|||
noPermissions: 'No permissions assigned', |
|||
contentType: 'Content Type', |
|||
model: 'Model', |
|||
appLabel: 'App Label', |
|||
confirmDelete: 'Are you sure you want to delete {count} group? | Are you sure you want to delete {count} groups?', |
|||
selectPermissions: 'Search and select permissions', |
|||
selectedPermissions: 'Selected Permissions', |
|||
groups: 'Groups', |
|||
selectedGroups: 'Selected Groups', |
|||
selectGroups: 'Search and select groups', |
|||
} |
@ -1,4 +1,5 @@ |
|||
export default { |
|||
projects: 'projects', |
|||
users: 'users', |
|||
groups: 'groups' |
|||
} |
@ -0,0 +1,141 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-card-title> |
|||
<v-btn |
|||
icon |
|||
class="mr-2" |
|||
@click="goBack" |
|||
> |
|||
<v-icon>{{ mdiArrowLeft }}</v-icon> |
|||
</v-btn> |
|||
{{ group.name }} |
|||
<v-spacer></v-spacer> |
|||
<v-btn |
|||
v-if="isStaff" |
|||
class="text-capitalize ms-2" |
|||
color="primary" |
|||
outlined |
|||
@click="dialogUpdate = true" |
|||
> |
|||
{{ $t('generic.edit') }} |
|||
</v-btn> |
|||
<v-dialog v-model="dialogUpdate" max-width="800px"> |
|||
<form-update |
|||
:group="group" |
|||
@cancel="dialogUpdate = false" |
|||
@updated="handleUpdated" |
|||
/> |
|||
</v-dialog> |
|||
</v-card-title> |
|||
|
|||
<v-card-text> |
|||
<v-row> |
|||
<v-col cols="12" sm="6"> |
|||
<v-card outlined> |
|||
<v-card-title>{{ $t('group.details') }}</v-card-title> |
|||
<v-list-item> |
|||
<v-list-item-content> |
|||
<v-list-item-title>{{ $t('group.id') }}</v-list-item-title> |
|||
<v-list-item-subtitle>{{ group.id }}</v-list-item-subtitle> |
|||
</v-list-item-content> |
|||
</v-list-item> |
|||
<v-list-item> |
|||
<v-list-item-content> |
|||
<v-list-item-title>{{ $t('group.name') }}</v-list-item-title> |
|||
<v-list-item-subtitle>{{ group.name }}</v-list-item-subtitle> |
|||
</v-list-item-content> |
|||
</v-list-item> |
|||
</v-card> |
|||
</v-col> |
|||
<v-col cols="12" sm="6"> |
|||
<v-card outlined> |
|||
<v-card-title>{{ $t('group.permissions') }}</v-card-title> |
|||
<v-card-text v-if="isLoading"> |
|||
<v-progress-circular indeterminate color="primary"></v-progress-circular> |
|||
</v-card-text> |
|||
<v-card-text v-else> |
|||
<v-chip |
|||
v-for="(permission, id) in permissionsToDisplay" |
|||
:key="id" |
|||
color="primary" |
|||
small |
|||
class="ma-1" |
|||
> |
|||
{{ permission.name }} |
|||
</v-chip> |
|||
<div v-if="!permissionsToDisplay || Object.keys(permissionsToDisplay).length === 0" class="text-center grey--text"> |
|||
{{ $t('group.noPermissions') }} |
|||
</div> |
|||
</v-card-text> |
|||
</v-card> |
|||
</v-col> |
|||
</v-row> |
|||
</v-card-text> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import { mapGetters } from 'vuex' |
|||
import { mdiArrowLeft } from '@mdi/js' |
|||
import { GroupDetails, Permission } from '@/domain/models/group/group' |
|||
import FormUpdate from '@/components/groups/FormUpdate.vue' |
|||
|
|||
export default Vue.extend({ |
|||
components: { |
|||
FormUpdate |
|||
}, |
|||
|
|||
layout: 'projects', |
|||
|
|||
middleware: ['check-auth', 'auth'], |
|||
|
|||
data() { |
|||
return { |
|||
group: {} as GroupDetails, |
|||
isLoading: false, |
|||
dialogUpdate: false, |
|||
permissions: [] as Permission[], |
|||
mdiArrowLeft |
|||
} |
|||
}, |
|||
|
|||
computed: { |
|||
...mapGetters('auth', ['isStaff']), |
|||
|
|||
permissionsToDisplay() { |
|||
return this.group.permission_names || {} |
|||
} |
|||
}, |
|||
|
|||
async fetch() { |
|||
this.isLoading = true |
|||
try { |
|||
const id = Number(this.$route.params.id) |
|||
this.group = await this.$services.group.getGroup(id) |
|||
} catch (e) { |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'error', |
|||
text: 'Failed to load group details' |
|||
}) |
|||
} finally { |
|||
this.isLoading = false |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
goBack() { |
|||
this.$router.push(this.localePath('/groups')) |
|||
}, |
|||
|
|||
handleUpdated() { |
|||
this.dialogUpdate = false |
|||
this.$fetch() |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'success', |
|||
text: 'Group updated successfully' |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
</script> |
@ -0,0 +1,161 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-card-title v-if="isStaff"> |
|||
<v-btn |
|||
class="text-capitalize ms-2" |
|||
:disabled="!canDelete" |
|||
outlined |
|||
@click.stop="dialogDelete = true" |
|||
> |
|||
{{ $t('generic.delete') }} |
|||
</v-btn> |
|||
<v-spacer></v-spacer> |
|||
<v-btn |
|||
class="text-capitalize ms-2" |
|||
color="primary" |
|||
outlined |
|||
@click="dialogCreate = true" |
|||
> |
|||
{{ $t('generic.create') }} |
|||
</v-btn> |
|||
<v-dialog v-model="dialogDelete" max-width="500"> |
|||
<form-delete :selected="selected" @cancel="dialogDelete = false" @remove="remove" /> |
|||
</v-dialog> |
|||
<v-dialog v-model="dialogCreate" max-width="800"> |
|||
<form-create @cancel="dialogCreate = false" @created="handleCreated" /> |
|||
</v-dialog> |
|||
</v-card-title> |
|||
<group-list |
|||
v-model="selected" |
|||
:items="groups" |
|||
:is-loading="isLoading" |
|||
:total="total" |
|||
@update:query="updateQuery" |
|||
/> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import _ from 'lodash' |
|||
import Vue from 'vue' |
|||
import { mapGetters } from 'vuex' |
|||
import GroupList from '@/components/groups/GroupList.vue' |
|||
import FormDelete from '@/components/groups/FormDelete.vue' |
|||
import FormCreate from '@/components/groups/FormCreate.vue' |
|||
import { Group } from '@/domain/models/group/group' |
|||
|
|||
export default Vue.extend({ |
|||
components: { |
|||
GroupList, |
|||
FormDelete, |
|||
FormCreate |
|||
}, |
|||
layout: 'projects', |
|||
|
|||
middleware: ['check-auth', 'auth'], |
|||
|
|||
data() { |
|||
return { |
|||
dialogDelete: false, |
|||
dialogCreate: false, |
|||
groups: [] as Group[], |
|||
selected: [] as Group[], |
|||
isLoading: false, |
|||
total: 0 |
|||
} |
|||
}, |
|||
|
|||
async fetch() { |
|||
this.isLoading = true |
|||
try { |
|||
const query = this.buildQuery() |
|||
const list = await this.$services.group.listGroups(query) |
|||
this.groups = list.results |
|||
this.total = list.count |
|||
} catch (e) { |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'error', |
|||
text: 'Failed to load groups' |
|||
}) |
|||
} finally { |
|||
this.isLoading = false |
|||
} |
|||
}, |
|||
|
|||
computed: { |
|||
...mapGetters('auth', ['isStaff']), |
|||
canDelete(): boolean { |
|||
return this.selected.length > 0 |
|||
} |
|||
}, |
|||
|
|||
watch: { |
|||
'$route.query': _.debounce(function () { |
|||
// @ts-ignore |
|||
this.$fetch() |
|||
}, 500) |
|||
}, |
|||
|
|||
methods: { |
|||
buildQuery() { |
|||
const { q, limit, offset, ordering, orderBy } = this.$route.query |
|||
const params = new URLSearchParams() |
|||
|
|||
if (q) params.append('search', q as string) |
|||
if (limit) params.append('limit', limit as string) |
|||
if (offset) params.append('offset', offset as string) |
|||
|
|||
if (ordering && orderBy) { |
|||
const direction = orderBy === '-' ? '-' : '' |
|||
params.append('ordering', `${direction}${ordering}`) |
|||
} |
|||
|
|||
return params.toString() |
|||
}, |
|||
|
|||
async remove() { |
|||
try { |
|||
for (const group of this.selected) { |
|||
await this.$services.group.deleteGroup(group.id) |
|||
} |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'success', |
|||
text: 'Groups deleted successfully' |
|||
}) |
|||
this.$fetch() |
|||
} catch (e) { |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'error', |
|||
text: 'Failed to delete groups' |
|||
}) |
|||
} finally { |
|||
this.dialogDelete = false |
|||
this.selected = [] |
|||
} |
|||
}, |
|||
|
|||
updateQuery(query: { query: Record<string, string> }) { |
|||
this.$router.push({ |
|||
query: { |
|||
...query.query |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
handleCreated() { |
|||
this.dialogCreate = false |
|||
this.$fetch() |
|||
this.$store.dispatch('notification/setNotification', { |
|||
color: 'success', |
|||
text: 'Group created successfully' |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
::v-deep .v-dialog { |
|||
width: 800px; |
|||
} |
|||
</style> |
@ -0,0 +1,134 @@ |
|||
import ApiService from '@/services/api.service' |
|||
import { Group, GroupDetails, Permission, ContentType } from '@/domain/models/group/group' |
|||
import { PaginatedResponse } from '@/repositories/user/apiUserRepository' |
|||
|
|||
function toGroupModel(item: { [key: string]: any }): Group { |
|||
return new Group( |
|||
item.id, |
|||
item.name |
|||
) |
|||
} |
|||
|
|||
function toGroupDetailsModel(item: { [key: string]: any }): GroupDetails { |
|||
return new GroupDetails( |
|||
item.id, |
|||
item.name, |
|||
item.permissions, |
|||
item.permission_names |
|||
) |
|||
} |
|||
|
|||
function toPermissionModel(item: { [key: string]: any }): Permission { |
|||
return new Permission( |
|||
item.id, |
|||
item.name, |
|||
item.codename, |
|||
item.content_type, |
|||
item.label |
|||
) |
|||
} |
|||
|
|||
function toContentTypeModel(item: { [key: string]: any }): ContentType { |
|||
return new ContentType( |
|||
item.id, |
|||
item.app_label, |
|||
item.model |
|||
) |
|||
} |
|||
|
|||
function toGroupModelList(response: PaginatedResponse<any>): PaginatedResponse<Group> { |
|||
return { |
|||
count: response.count, |
|||
next: response.next, |
|||
previous: response.previous, |
|||
results: response.results.map((item: any) => toGroupModel(item)) |
|||
} |
|||
} |
|||
|
|||
function toPermissionModelList(response: PaginatedResponse<any>): PaginatedResponse<Permission> { |
|||
return { |
|||
count: response.count, |
|||
next: response.next, |
|||
previous: response.previous, |
|||
results: response.results.map((item: any) => toPermissionModel(item)) |
|||
} |
|||
} |
|||
|
|||
function toContentTypeModelList(response: PaginatedResponse<any>): PaginatedResponse<ContentType> { |
|||
return { |
|||
count: response.count, |
|||
next: response.next, |
|||
previous: response.previous, |
|||
results: response.results.map((item: any) => toContentTypeModel(item)) |
|||
} |
|||
} |
|||
|
|||
export class APIGroupRepository { |
|||
constructor(private readonly request = ApiService) { } |
|||
|
|||
// Group methods
|
|||
async listGroups(query: string = ''): Promise<PaginatedResponse<Group>> { |
|||
const url = `/groups/?${query}` |
|||
const response = await this.request.get(url) |
|||
return toGroupModelList(response.data) |
|||
} |
|||
|
|||
async getGroup(id: number): Promise<GroupDetails> { |
|||
const url = `/groups/${id}/` |
|||
const response = await this.request.get(url) |
|||
return toGroupDetailsModel(response.data) |
|||
} |
|||
|
|||
async createGroup(data: { name: string, permissions?: number[] }): Promise<Group> { |
|||
const url = '/groups/' |
|||
// Ensure permissions is sent as an array even if empty
|
|||
const payload = { |
|||
name: data.name, |
|||
permissions: data.permissions || [] |
|||
} |
|||
const response = await this.request.post(url, payload) |
|||
return toGroupModel(response.data) |
|||
} |
|||
|
|||
async updateGroup(id: number, data: { name?: string, permissions?: number[] }): Promise<Group> { |
|||
const url = `/groups/${id}/` |
|||
// Ensure permissions is sent as an array even if empty
|
|||
const payload = { ...data } |
|||
if (!payload.permissions) { |
|||
payload.permissions = [] |
|||
} |
|||
const response = await this.request.patch(url, payload) |
|||
return toGroupModel(response.data) |
|||
} |
|||
|
|||
async deleteGroup(id: number): Promise<void> { |
|||
const url = `/groups/${id}/` |
|||
await this.request.delete(url) |
|||
} |
|||
|
|||
// Permission methods
|
|||
async listPermissions(query: string = ''): Promise<PaginatedResponse<Permission>> { |
|||
const url = `/permissions/?${query}` |
|||
const response = await this.request.get(url) |
|||
return toPermissionModelList(response.data) |
|||
} |
|||
|
|||
async getPermission(id: number): Promise<Permission> { |
|||
const url = `/permissions/${id}/` |
|||
const response = await this.request.get(url) |
|||
return toPermissionModel(response.data) |
|||
} |
|||
|
|||
// ContentType methods
|
|||
async listContentTypes(query: string = ''): Promise<PaginatedResponse<ContentType>> { |
|||
const url = `/content-types/?${query}` |
|||
const response = await this.request.get(url) |
|||
return toContentTypeModelList(response.data) |
|||
} |
|||
|
|||
async getContentType(id: number): Promise<ContentType> { |
|||
const url = `/content-types/${id}/` |
|||
const response = await this.request.get(url) |
|||
return toContentTypeModel(response.data) |
|||
} |
|||
} |
@ -0,0 +1,82 @@ |
|||
import { Group, GroupDetails, Permission, ContentType } from '@/domain/models/group/group' |
|||
import { APIGroupRepository } from '@/repositories/group/apiGroupRepository' |
|||
import { PaginatedResponse } from '@/repositories/user/apiUserRepository' |
|||
|
|||
export class GroupApplicationService { |
|||
constructor(private readonly repository: APIGroupRepository) { } |
|||
|
|||
// Group methods
|
|||
public async listGroups(query: string = ''): Promise<PaginatedResponse<Group>> { |
|||
try { |
|||
return await this.repository.listGroups(query) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || 'Failed to fetch groups') |
|||
} |
|||
} |
|||
|
|||
public async getGroup(id: number): Promise<GroupDetails> { |
|||
try { |
|||
return await this.repository.getGroup(id) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || `Failed to fetch group with ID ${id}`) |
|||
} |
|||
} |
|||
|
|||
public async createGroup(data: { name: string, permissions?: number[] }): Promise<Group> { |
|||
try { |
|||
return await this.repository.createGroup(data) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || 'Failed to create group') |
|||
} |
|||
} |
|||
|
|||
public async updateGroup(id: number, data: { name?: string, permissions?: number[] }): Promise<Group> { |
|||
try { |
|||
return await this.repository.updateGroup(id, data) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || `Failed to update group with ID ${id}`) |
|||
} |
|||
} |
|||
|
|||
public async deleteGroup(id: number): Promise<void> { |
|||
try { |
|||
await this.repository.deleteGroup(id) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || `Failed to delete group with ID ${id}`) |
|||
} |
|||
} |
|||
|
|||
// Permission methods
|
|||
public async listPermissions(query: string = ''): Promise<PaginatedResponse<Permission>> { |
|||
try { |
|||
return await this.repository.listPermissions(query) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || 'Failed to fetch permissions') |
|||
} |
|||
} |
|||
|
|||
public async getPermission(id: number): Promise<Permission> { |
|||
try { |
|||
return await this.repository.getPermission(id) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || `Failed to fetch permission with ID ${id}`) |
|||
} |
|||
} |
|||
|
|||
// ContentType methods
|
|||
public async listContentTypes(query: string = ''): Promise<PaginatedResponse<ContentType>> { |
|||
try { |
|||
return await this.repository.listContentTypes(query) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || 'Failed to fetch content types') |
|||
} |
|||
} |
|||
|
|||
public async getContentType(id: number): Promise<ContentType> { |
|||
try { |
|||
return await this.repository.getContentType(id) |
|||
} catch (e: any) { |
|||
throw new Error(e.response?.data?.detail || `Failed to fetch content type with ID ${id}`) |
|||
} |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save