diff --git a/accounts/urls.py b/accounts/urls.py index c602fcb..30fc3b0 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -7,11 +7,22 @@ change_email_view, change_password_view, disconnect_social_account, + label_create_view, + label_delete_view, + label_edit_view, + label_list_view, + label_permission_grant_view, + label_permission_revoke_view, + label_permissions_view, logout_view, merge_agree_view, merge_reject_view, merge_request_view, merge_review_view, + org_label_create_view, + org_label_delete_view, + org_label_edit_view, + org_label_list_view, organization_create, organization_delete, organization_detail, @@ -145,4 +156,45 @@ organization_member_remove, name="organization_member_remove", ), + # Organization Label URLs + path( + "organizations//labels/", + org_label_list_view, + name="org_label_list", + ), + path( + "organizations//labels/create/", + org_label_create_view, + name="org_label_create", + ), + path( + "organizations//labels//edit/", + org_label_edit_view, + name="org_label_edit", + ), + path( + "organizations//labels//delete/", + org_label_delete_view, + name="org_label_delete", + ), + # Label management URLs + path("labels/", label_list_view, name="label_list"), + path("labels/create/", label_create_view, name="label_create"), + path("labels//edit/", label_edit_view, name="label_edit"), + path("labels//delete/", label_delete_view, name="label_delete"), + path( + "labels//permissions/", + label_permissions_view, + name="label_permissions", + ), + path( + "labels//permissions/grant/", + label_permission_grant_view, + name="label_permission_grant", + ), + path( + "permissions//revoke/", + label_permission_revoke_view, + name="label_permission_revoke", + ), ] diff --git a/accounts/views.py b/accounts/views.py index 9651db5..a7c0402 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -142,8 +142,16 @@ def sign_up_view(request): @login_required def profile_view(request): """Display user profile page.""" + from labels.models import Label, OwnerType + profile, _created = UserProfile.objects.get_or_create(user=request.user) - return render(request, "profile.html", {"profile": profile}) + # 获取用户拥有的标签数量 + label_count = Label.objects.filter( + owner_type=OwnerType.USER, owner_id=request.user.id + ).count() + return render( + request, "profile.html", {"profile": profile, "label_count": label_count} + ) def _get_profile_edit_forms( @@ -1594,3 +1602,629 @@ def organization_delete(request, slug): transaction.on_commit(lambda f=avatar_file: f.delete(save=False)) messages.success(request, f"组织 {org_name} 已删除。") return redirect("accounts:organization_list") + + +# Label management views + + +@login_required +def label_list_view(request): + """Display user's labels.""" + from labels.models import Label, OwnerType + + # Get user's labels + user_labels = Label.objects.filter( + owner_type=OwnerType.USER, owner_id=request.user.id + ).order_by("-created_at") + + # Get labels shared with user + shared_labels = [] + from labels.models import GranteeType, LabelPermission + + label_permissions = LabelPermission.objects.filter( + grantee_type=GranteeType.USER, grantee_id=request.user.id, is_active=True + ).select_related("label") + + for perm in label_permissions: + if not perm.is_expired(): + label = perm.label + label.permission_level = perm.permission_level + shared_labels.append(label) + + return render( + request, + "accounts/label_list.html", + {"user_labels": user_labels, "shared_labels": shared_labels}, + ) + + +@login_required +def label_create_view(request): + """Create a new label.""" + import json + + from labels.models import Label, LabelType, OwnerType + + if request.method == "POST": + name = request.POST.get("name", "").strip() + name_zh = request.POST.get("name_zh", "").strip() + label_type = request.POST.get("type", "") + is_public = request.POST.get("is_public") == "1" + data_str = request.POST.get("data", "{}").strip() + + errors = {} + if not name: + errors["name"] = "英文名称不能为空" + if not name_zh: + errors["name_zh"] = "中文名称不能为空" + if label_type not in dict(LabelType.choices): + errors["type"] = "请选择有效的标签类型" + + # Parse JSON data + data = {} + if data_str: + try: + data = json.loads(data_str) + if not isinstance(data, dict): + errors["data"] = "标签数据必须是 JSON 对象格式" + except json.JSONDecodeError: + errors["data"] = "标签数据格式无效,请输入有效的 JSON" + + # Check uniqueness + if ( + name + and Label.objects.filter( + name=name, owner_type=OwnerType.USER, owner_id=request.user.id + ).exists() + ): + errors["name"] = "该标签名称已存在" + + if not errors: + label = Label.objects.create( + name=name, + name_zh=name_zh, + type=label_type, + owner_type=OwnerType.USER, + owner_id=request.user.id, + is_public=is_public, + data=data, + ) + messages.success(request, f"标签 {label.name_zh} 创建成功") + return redirect("accounts:label_list") + + context = { + "errors": errors, + "name": name, + "name_zh": name_zh, + "type": label_type, + "is_public": is_public, + "data": data_str, + "label_types": LabelType.choices, + } + return render(request, "accounts/label_form.html", context) + + return render( + request, "accounts/label_form.html", {"label_types": LabelType.choices} + ) + + +@login_required +def label_edit_view(request, label_id): + """Edit an existing label.""" + import json + + from labels.models import Label, LabelType + + label = get_object_or_404(Label, id=label_id) + + if not label.can_edit(request.user): + messages.error(request, "您没有权限编辑此标签") + return redirect("accounts:label_list") + + if request.method == "POST": + name = request.POST.get("name", "").strip() + name_zh = request.POST.get("name_zh", "").strip() + label_type = request.POST.get("type", "") + is_public = request.POST.get("is_public") == "1" + data_str = request.POST.get("data", "{}").strip() + + errors = {} + if not name: + errors["name"] = "英文名称不能为空" + if not name_zh: + errors["name_zh"] = "中文名称不能为空" + if label_type not in dict(LabelType.choices): + errors["type"] = "请选择有效的标签类型" + + # Parse JSON data + data = {} + if data_str: + try: + data = json.loads(data_str) + if not isinstance(data, dict): + errors["data"] = "标签数据必须是 JSON 对象格式" + except json.JSONDecodeError: + errors["data"] = "标签数据格式无效,请输入有效的 JSON" + + # Check uniqueness (exclude current label) + if ( + name + and Label.objects.filter( + name=name, owner_type=label.owner_type, owner_id=label.owner_id + ) + .exclude(id=label.id) + .exists() + ): + errors["name"] = "该标签名称已存在" + + if not errors: + label.name = name + label.name_zh = name_zh + label.type = label_type + label.is_public = is_public + label.data = data + label.save() + messages.success(request, f"标签 {label.name_zh} 更新成功") + return redirect("accounts:label_list") + + context = { + "label": label, + "errors": errors, + "name": name, + "name_zh": name_zh, + "type": label_type, + "is_public": is_public, + "data": data_str, + "label_types": LabelType.choices, + } + return render(request, "accounts/label_form.html", context) + + return render( + request, + "accounts/label_form.html", + {"label": label, "label_types": LabelType.choices}, + ) + + +@login_required +@require_POST +def label_delete_view(request, label_id): + """Delete a label.""" + from labels.models import Label + + label = get_object_or_404(Label, id=label_id) + + if not label.can_manage(request.user): + messages.error(request, "您没有权限删除此标签") + return redirect("accounts:label_list") + + label_name = label.name_zh + label.delete() + messages.success(request, f"标签 {label_name} 已删除") + return redirect("accounts:label_list") + + +@login_required +def label_permissions_view(request, label_id): + """Manage label permissions.""" + from labels.models import Label, LabelPermission + + label = get_object_or_404(Label, id=label_id) + + if not label.can_manage(request.user): + messages.error(request, "您没有权限管理此标签的权限") + return redirect("accounts:label_list") + + permissions = LabelPermission.objects.filter(label=label, is_active=True).order_by( + "-granted_at" + ) + + return render( + request, + "accounts/label_permissions.html", + {"label": label, "permissions": permissions}, + ) + + +@login_required +def label_permission_grant_view(request, label_id): + """Grant permission to a user.""" + from labels.models import ( + GranteeType, + Label, + LabelPermission, + LabelPermissionLog, + PermissionLevel, + ) + + label = get_object_or_404(Label, id=label_id) + + if not label.can_manage(request.user): + messages.error(request, "您没有权限管理此标签的权限") + return redirect("accounts:label_list") + + if request.method == "POST": + username = request.POST.get("username", "").strip() + permission_level = request.POST.get("permission_level", "") + expires_days = request.POST.get("expires_days", "").strip() + notes = request.POST.get("notes", "").strip() + + errors = {} + if not username: + errors["username"] = "请输入用户名" + if permission_level not in dict(PermissionLevel.choices): + errors["permission_level"] = "请选择有效的权限级别" + + # Find user + User = get_user_model() + try: + target_user = User.objects.get(username=username) + except User.DoesNotExist: + errors["username"] = f"用户 {username} 不存在" + target_user = None + + # Check if user is trying to grant permission to themselves + if target_user and target_user.id == request.user.id: + errors["username"] = "不能授权给自己" + target_user = None + + # Calculate expiration date + expires_at = None + if expires_days: + try: + days = int(expires_days) + if days > 0: + expires_at = timezone.now() + timedelta(days=days) + except ValueError: + errors["expires_days"] = "请输入有效的天数" + + if not errors and target_user: + # Check if permission already exists + existing_perm = LabelPermission.objects.filter( + label=label, + grantee_type=GranteeType.USER, + grantee_id=target_user.id, + is_active=True, + ).first() + + if existing_perm: + # Update existing permission + existing_perm.permission_level = permission_level + existing_perm.expires_at = expires_at + existing_perm.notes = notes + existing_perm.save() + + # Log the update + LabelPermissionLog.objects.create( + permission=existing_perm, + action="updated", + actor=request.user, + details={ + "permission_level": permission_level, + "expires_at": expires_at.isoformat() if expires_at else None, + }, + ) + messages.success(request, f"已更新授予 {username} 的权限") + else: + # Create new permission + perm = LabelPermission.objects.create( + label=label, + grantee_type=GranteeType.USER, + grantee_id=target_user.id, + permission_level=permission_level, + granted_by=request.user, + expires_at=expires_at, + notes=notes, + ) + + # Log the grant + LabelPermissionLog.objects.create( + permission=perm, + action="granted", + actor=request.user, + details={ + "permission_level": permission_level, + "expires_at": expires_at.isoformat() if expires_at else None, + }, + ) + messages.success(request, f"已授予 {username} 权限") + + return redirect("accounts:label_permissions", label_id=label.id) + + context = { + "label": label, + "errors": errors, + "username": username, + "permission_level": permission_level, + "expires_days": expires_days, + "notes": notes, + "permission_levels": PermissionLevel.choices, + } + return render(request, "accounts/label_permission_grant.html", context) + + return render( + request, + "accounts/label_permission_grant.html", + {"label": label, "permission_levels": PermissionLevel.choices}, + ) + + +@login_required +@require_POST +def label_permission_revoke_view(request, permission_id): + """Revoke a permission.""" + from labels.models import LabelPermission, LabelPermissionLog + + permission = get_object_or_404(LabelPermission, id=permission_id) + + if not permission.label.can_manage(request.user): + messages.error(request, "您没有权限撤销此权限") + return redirect("accounts:label_list") + + label_id = permission.label.id + permission.is_active = False + permission.save() + + # Log the revocation + LabelPermissionLog.objects.create( + permission=permission, action="revoked", actor=request.user, details={} + ) + + messages.success(request, "权限已撤销") + return redirect("accounts:label_permissions", label_id=label_id) + + +# Organization Label Views + + +def _check_org_label_permission(request, slug, require_admin=False): + """Check if user has permission to manage organization labels.""" + organization = get_object_or_404(Organization, slug=slug) + + try: + membership = OrganizationMembership.objects.get( + user=request.user, organization=organization + ) + except OrganizationMembership.DoesNotExist: + return None, None, "您不是该组织的成员" + + if require_admin and not membership.is_admin_or_owner(): + return organization, membership, "只有管理员可以执行此操作" + + return organization, membership, None + + +@login_required +def org_label_list_view(request, slug): + """List labels for an organization.""" + from labels.models import Label, OwnerType + + organization, membership, error = _check_org_label_permission(request, slug) + if error: + messages.error(request, error) + return redirect("accounts:organization_list") + + org_labels = Label.objects.filter( + owner_type=OwnerType.ORGANIZATION, owner_id=organization.id + ).order_by("-created_at") + + return render( + request, + "accounts/org_label_list.html", + { + "organization": organization, + "membership": membership, + "org_labels": org_labels, + "is_admin": membership.is_admin_or_owner(), + }, + ) + + +@login_required +def org_label_create_view(request, slug): + """Create a new label for an organization.""" + import json + + from labels.models import Label, LabelType, OwnerType + + organization, membership, error = _check_org_label_permission( + request, slug, require_admin=True + ) + if error: + messages.error(request, error) + return redirect("accounts:organization_list") + + if request.method == "POST": + name = request.POST.get("name", "").strip() + name_zh = request.POST.get("name_zh", "").strip() + label_type = request.POST.get("type", "") + is_public = request.POST.get("is_public") == "1" + data_str = request.POST.get("data", "{}").strip() + + errors = {} + if not name: + errors["name"] = "英文名称不能为空" + if not name_zh: + errors["name_zh"] = "中文名称不能为空" + if label_type not in dict(LabelType.choices): + errors["type"] = "请选择有效的标签类型" + + # Parse JSON data + data = {} + if data_str: + try: + data = json.loads(data_str) + if not isinstance(data, dict): + errors["data"] = "标签数据必须是 JSON 对象格式" + except json.JSONDecodeError: + errors["data"] = "标签数据格式无效,请输入有效的 JSON" + + # Check uniqueness within organization + if ( + name + and Label.objects.filter( + name=name, owner_type=OwnerType.ORGANIZATION, owner_id=organization.id + ).exists() + ): + errors["name"] = "该标签名称已存在" + + if not errors: + Label.objects.create( + name=name, + name_zh=name_zh, + type=label_type, + owner_type=OwnerType.ORGANIZATION, + owner_id=organization.id, + is_public=is_public, + data=data, + ) + messages.success(request, f"标签 {name_zh} 创建成功") + return redirect("accounts:org_label_list", slug=slug) + + context = { + "organization": organization, + "membership": membership, + "errors": errors, + "name": name, + "name_zh": name_zh, + "type": label_type, + "is_public": is_public, + "data": data_str, + "label_types": LabelType.choices, + "is_admin": membership.is_admin_or_owner(), + } + return render(request, "accounts/org_label_form.html", context) + + from labels.models import LabelType + + return render( + request, + "accounts/org_label_form.html", + { + "organization": organization, + "membership": membership, + "label_types": LabelType.choices, + "is_admin": membership.is_admin_or_owner(), + }, + ) + + +@login_required +def org_label_edit_view(request, slug, label_id): + """Edit an existing organization label.""" + import json + + from labels.models import Label, LabelType, OwnerType + + organization, membership, error = _check_org_label_permission( + request, slug, require_admin=True + ) + if error: + messages.error(request, error) + return redirect("accounts:organization_list") + + label = get_object_or_404( + Label, + id=label_id, + owner_type=OwnerType.ORGANIZATION, + owner_id=organization.id, + ) + + if request.method == "POST": + name = request.POST.get("name", "").strip() + name_zh = request.POST.get("name_zh", "").strip() + label_type = request.POST.get("type", "") + is_public = request.POST.get("is_public") == "1" + data_str = request.POST.get("data", "{}").strip() + + errors = {} + if not name: + errors["name"] = "英文名称不能为空" + if not name_zh: + errors["name_zh"] = "中文名称不能为空" + if label_type not in dict(LabelType.choices): + errors["type"] = "请选择有效的标签类型" + + # Parse JSON data + data = {} + if data_str: + try: + data = json.loads(data_str) + if not isinstance(data, dict): + errors["data"] = "标签数据必须是 JSON 对象格式" + except json.JSONDecodeError: + errors["data"] = "标签数据格式无效,请输入有效的 JSON" + + # Check uniqueness (exclude current label) + if ( + name + and Label.objects.filter( + name=name, owner_type=OwnerType.ORGANIZATION, owner_id=organization.id + ) + .exclude(id=label.id) + .exists() + ): + errors["name"] = "该标签名称已存在" + + if not errors: + label.name = name + label.name_zh = name_zh + label.type = label_type + label.is_public = is_public + label.data = data + label.save() + messages.success(request, f"标签 {label.name_zh} 更新成功") + return redirect("accounts:org_label_list", slug=slug) + + context = { + "organization": organization, + "membership": membership, + "label": label, + "errors": errors, + "name": name, + "name_zh": name_zh, + "type": label_type, + "is_public": is_public, + "data": data_str, + "label_types": LabelType.choices, + "is_admin": membership.is_admin_or_owner(), + } + return render(request, "accounts/org_label_form.html", context) + + return render( + request, + "accounts/org_label_form.html", + { + "organization": organization, + "membership": membership, + "label": label, + "label_types": LabelType.choices, + "is_admin": membership.is_admin_or_owner(), + }, + ) + + +@login_required +@require_POST +def org_label_delete_view(request, slug, label_id): + """Delete an organization label.""" + from labels.models import Label, OwnerType + + organization, _membership, error = _check_org_label_permission( + request, slug, require_admin=True + ) + if error: + messages.error(request, error) + return redirect("accounts:organization_list") + + label = get_object_or_404( + Label, + id=label_id, + owner_type=OwnerType.ORGANIZATION, + owner_id=organization.id, + ) + + label_name = label.name_zh + label.delete() + messages.success(request, f"标签 {label_name} 已删除") + return redirect("accounts:org_label_list", slug=slug) diff --git a/config/settings.py b/config/settings.py index 1d27f22..f110c8a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -129,6 +129,7 @@ "shop", "chdb", "messages.apps.SiteMessagesConfig", + "labels", ] _BASE_MIDDLEWARE = [ diff --git a/labels/__init__.py b/labels/__init__.py new file mode 100644 index 0000000..161d33a --- /dev/null +++ b/labels/__init__.py @@ -0,0 +1,3 @@ +"""Labels app package configuration.""" + +default_app_config = "labels.apps.LabelsConfig" diff --git a/labels/admin.py b/labels/admin.py new file mode 100644 index 0000000..2f42f93 --- /dev/null +++ b/labels/admin.py @@ -0,0 +1,391 @@ +"""Admin customisations for managing labels and their permissions.""" + +import json + +from django.contrib import admin +from django.http import HttpResponse +from django.utils.html import format_html + +from .models import Label, LabelPermission, LabelPermissionLog + + +class LabelPermissionInline(admin.TabularInline): + """标签权限内联编辑.""" + + model = LabelPermission + extra = 0 + fields = [ + "grantee_type", + "grantee_id", + "permission_level", + "granted_by", + "expires_at", + "is_active", + ] + readonly_fields = ["granted_by", "granted_at"] + + def get_queryset(self, request): + """只显示激活的权限.""" + qs = super().get_queryset(request) + return qs.filter(is_active=True) + + +@admin.register(Label) +class LabelAdmin(admin.ModelAdmin): + """标签管理后台.""" + + list_display = [ + "id", + "name", + "name_zh", + "type", + "owner_type", + "owner_display", + "entity_count", + "is_public", + "sync_source", + "created_at", + ] + + list_filter = [ + "type", + "owner_type", + "is_public", + "sync_source", + "created_at", + ] + + search_fields = [ + "name", + "name_zh", + "owner_id", + ] + + readonly_fields = [ + "created_at", + "updated_at", + "last_synced_at", + "sync_source", + "entity_count_detail", + "data_preview", + ] + + fieldsets = ( + ("基本信息", {"fields": ("name", "name_zh", "type")}), + ("所有者信息", {"fields": ("owner_type", "owner_id", "is_public")}), + ( + "标签数据", + { + "fields": ("data", "data_preview", "entity_count_detail"), + "classes": ("collapse",), + }, + ), + ( + "同步信息", + { + "fields": ("sync_source", "last_synced_at"), + "classes": ("collapse",), + }, + ), + ( + "时间戳", + { + "fields": ("created_at", "updated_at"), + "classes": ("collapse",), + }, + ), + ) + + inlines = [LabelPermissionInline] + + actions = ["make_public", "make_private", "export_labels"] + + def owner_display(self, obj): + """显示所有者信息.""" + if obj.owner_type == "system": + return format_html('系统') + elif obj.owner_type == "user": + from accounts.models import User + + try: + user = User.objects.get(id=obj.owner_id) + return format_html( + '{}', + obj.owner_id, + user.username, + ) + except User.DoesNotExist: + return f"用户 #{obj.owner_id} (已删除)" + elif obj.owner_type == "organization": + # 假设有 Organization 模型 + return f"组织 #{obj.owner_id}" + return "-" + + owner_display.short_description = "所有者" + + def entity_count(self, obj): + """计算标签包含的实体总数.""" + count = 0 + for platform in obj.data.get("platforms", []): + count += len(platform.get("orgs", [])) + count += len(platform.get("repos", [])) + count += len(platform.get("developers", [])) + return count + + entity_count.short_description = "实体数量" + + def entity_count_detail(self, obj): + """详细的实体计数.""" + details = [] + for platform in obj.data.get("platforms", []): + platform_name = platform.get("name", "Unknown") + org_count = len(platform.get("orgs", [])) + repo_count = len(platform.get("repos", [])) + dev_count = len(platform.get("developers", [])) + details.append( + f"{platform_name}: {org_count} 组织, {repo_count} 仓库, {dev_count} 开发者" + ) + return "\n".join(details) if details else "无数据" + + entity_count_detail.short_description = "实体详情" + + def data_preview(self, obj): + """JSON 数据预览.""" + return format_html( + '
{}
', + json.dumps(obj.data, indent=2, ensure_ascii=False), + ) + + data_preview.short_description = "数据预览" + + def make_public(self, request, queryset): + """批量设为公开.""" + updated = queryset.update(is_public=True) + self.message_user(request, f"成功将 {updated} 个标签设为公开") + + make_public.short_description = "设为公开" + + def make_private(self, request, queryset): + """批量设为私有.""" + updated = queryset.update(is_public=False) + self.message_user(request, f"成功将 {updated} 个标签设为私有") + + make_private.short_description = "设为私有" + + def export_labels(self, request, queryset): + """导出标签为 JSON.""" + labels_data = [] + for label in queryset: + labels_data.append( + { + "name": label.name, + "name_zh": label.name_zh, + "type": label.type, + "data": label.data, + } + ) + + response = HttpResponse( + json.dumps(labels_data, indent=2, ensure_ascii=False), + content_type="application/json", + ) + response["Content-Disposition"] = 'attachment; filename="labels_export.json"' + return response + + export_labels.short_description = "导出选中标签" + + +@admin.register(LabelPermission) +class LabelPermissionAdmin(admin.ModelAdmin): + """标签权限管理后台.""" + + list_display = [ + "id", + "label_link", + "grantee_display", + "permission_level", + "status_display", + "granted_by_display", + "granted_at", + "expires_at", + ] + + list_filter = [ + "permission_level", + "grantee_type", + "is_active", + "granted_at", + ] + + search_fields = [ + "label__name", + "label__name_zh", + "grantee_id", + "notes", + ] + + readonly_fields = [ + "granted_at", + "granted_by", + ] + + fieldsets = ( + ( + "权限信息", + {"fields": ("label", "grantee_type", "grantee_id", "permission_level")}, + ), + ( + "授权信息", + {"fields": ("granted_by", "granted_at", "expires_at", "is_active")}, + ), + ( + "备注", + {"fields": ("notes",)}, + ), + ) + + actions = ["activate_permissions", "deactivate_permissions"] + + def label_link(self, obj): + """标签链接.""" + return format_html( + '{}', + obj.label.id, + obj.label.name, + ) + + label_link.short_description = "标签" + + def grantee_display(self, obj): + """被授权对象显示.""" + if obj.grantee_type == "user": + from accounts.models import User + + try: + user = User.objects.get(id=obj.grantee_id) + return format_html( + '用户: {}', + obj.grantee_id, + user.username, + ) + except User.DoesNotExist: + return f"用户 #{obj.grantee_id} (已删除)" + elif obj.grantee_type == "organization": + return f"组织 #{obj.grantee_id}" + return "-" + + grantee_display.short_description = "被授权对象" + + def granted_by_display(self, obj): + """授权者显示.""" + if not obj.granted_by: + return "-" + return format_html( + '{}', + obj.granted_by.id, + obj.granted_by.username, + ) + + granted_by_display.short_description = "授权者" + + def status_display(self, obj): + """权限状态显示.""" + if not obj.is_active: + return format_html('已撤销') + if obj.is_expired(): + return format_html('已过期') + return format_html('激活') + + status_display.short_description = "状态" + + def activate_permissions(self, request, queryset): + """激活权限.""" + updated = queryset.update(is_active=True) + self.message_user(request, f"成功激活 {updated} 个权限") + + activate_permissions.short_description = "激活选中权限" + + def deactivate_permissions(self, request, queryset): + """停用权限.""" + updated = queryset.update(is_active=False) + self.message_user(request, f"成功停用 {updated} 个权限") + + deactivate_permissions.short_description = "停用选中权限" + + +@admin.register(LabelPermissionLog) +class LabelPermissionLogAdmin(admin.ModelAdmin): + """标签权限日志管理后台.""" + + list_display = [ + "id", + "permission_link", + "action", + "actor_display", + "timestamp", + ] + + list_filter = [ + "action", + "timestamp", + ] + + search_fields = [ + "permission__label__name", + "actor__username", + ] + + readonly_fields = [ + "permission", + "action", + "actor", + "timestamp", + "details_display", + ] + + fieldsets = ( + ("日志信息", {"fields": ("permission", "action", "actor", "timestamp")}), + ( + "变更详情", + {"fields": ("details_display",)}, + ), + ) + + def has_add_permission(self, request): + """禁止添加日志(只能由系统创建).""" + return False + + def has_delete_permission(self, request, obj=None): + """禁止删除日志(审计需要).""" + return False + + def permission_link(self, obj): + """权限链接.""" + return format_html( + '权限 #{}', + obj.permission.id, + obj.permission.id, + ) + + permission_link.short_description = "权限" + + def actor_display(self, obj): + """操作者显示.""" + if not obj.actor: + return "系统" + return format_html( + '{}', + obj.actor.id, + obj.actor.username, + ) + + actor_display.short_description = "操作者" + + def details_display(self, obj): + """详情显示.""" + return format_html( + "
{}
", + json.dumps(obj.details, indent=2, ensure_ascii=False), + ) + + details_display.short_description = "变更详情" diff --git a/labels/apps.py b/labels/apps.py new file mode 100644 index 0000000..facbc20 --- /dev/null +++ b/labels/apps.py @@ -0,0 +1,11 @@ +"""Configuration for the labels Django application.""" + +from django.apps import AppConfig + + +class LabelsConfig(AppConfig): + """App configuration for the labels app.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "labels" + verbose_name = "标签管理" diff --git a/labels/migrations/0001_initial.py b/labels/migrations/0001_initial.py new file mode 100644 index 0000000..0884d55 --- /dev/null +++ b/labels/migrations/0001_initial.py @@ -0,0 +1,94 @@ +# Generated by Django 5.2.7 on 2025-12-25 09:27 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Label', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, max_length=200, verbose_name='英文名称')), + ('name_zh', models.CharField(max_length=200, verbose_name='中文名称')), + ('type', models.CharField(choices=[('project', '项目'), ('enterprise', '企业'), ('foundation', '基金会'), ('technology', '技术领域'), ('community', '社区')], db_index=True, max_length=20, verbose_name='标签类型')), + ('owner_type', models.CharField(choices=[('system', '系统'), ('user', '个人'), ('organization', '组织')], db_index=True, default='user', max_length=20, verbose_name='所有者类型')), + ('owner_id', models.IntegerField(blank=True, db_index=True, null=True, verbose_name='所有者ID')), + ('data', models.JSONField(default=dict, verbose_name='标签数据')), + ('is_public', models.BooleanField(default=False, verbose_name='是否公开')), + ('sync_source', models.CharField(blank=True, max_length=50, null=True, verbose_name='同步来源')), + ('last_synced_at', models.DateTimeField(blank=True, null=True, verbose_name='最后同步时间')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '标签', + 'verbose_name_plural': '标签', + 'db_table': 'labels', + 'indexes': [models.Index(fields=['owner_type', 'owner_id'], name='labels_owner_t_09f378_idx'), models.Index(fields=['type', 'is_public'], name='labels_type_c20be6_idx')], + 'constraints': [models.UniqueConstraint(fields=('name', 'owner_type', 'owner_id'), name='unique_label_per_owner')], + }, + ), + migrations.CreateModel( + name='LabelPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('grantee_type', models.CharField(choices=[('user', '用户'), ('organization', '组织')], max_length=20, verbose_name='被授权对象类型')), + ('grantee_id', models.IntegerField(verbose_name='被授权对象ID')), + ('permission_level', models.CharField(choices=[('view', '查看'), ('use', '使用'), ('edit', '编辑'), ('manage', '管理')], max_length=20, verbose_name='权限级别')), + ('granted_at', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')), + ('expires_at', models.DateTimeField(blank=True, null=True, verbose_name='过期时间')), + ('is_active', models.BooleanField(default=True, verbose_name='是否激活')), + ('notes', models.TextField(blank=True, verbose_name='备注')), + ('granted_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_permissions', to=settings.AUTH_USER_MODEL, verbose_name='授权者')), + ('label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='labels.label', verbose_name='标签')), + ], + options={ + 'verbose_name': '标签权限', + 'verbose_name_plural': '标签权限', + 'db_table': 'label_permissions', + }, + ), + migrations.CreateModel( + name='LabelPermissionLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(choices=[('granted', '授予'), ('updated', '更新'), ('revoked', '撤销'), ('expired', '过期')], max_length=20, verbose_name='操作')), + ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='时间戳')), + ('details', models.JSONField(default=dict, verbose_name='变更详情')), + ('actor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='操作者')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labels.labelpermission', verbose_name='权限')), + ], + options={ + 'verbose_name': '标签权限日志', + 'verbose_name_plural': '标签权限日志', + 'db_table': 'label_permission_logs', + 'ordering': ['-timestamp'], + }, + ), + migrations.AddIndex( + model_name='labelpermission', + index=models.Index(fields=['label', 'grantee_type', 'grantee_id'], name='label_permi_label_i_450297_idx'), + ), + migrations.AddIndex( + model_name='labelpermission', + index=models.Index(fields=['grantee_type', 'grantee_id', 'is_active'], name='label_permi_grantee_dc3eb4_idx'), + ), + migrations.AddIndex( + model_name='labelpermission', + index=models.Index(fields=['expires_at'], name='label_permi_expires_686383_idx'), + ), + migrations.AddConstraint( + model_name='labelpermission', + constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('label', 'grantee_type', 'grantee_id'), name='unique_active_permission'), + ), + ] diff --git a/labels/migrations/0002_alter_label_sync_source.py b/labels/migrations/0002_alter_label_sync_source.py new file mode 100644 index 0000000..dd16100 --- /dev/null +++ b/labels/migrations/0002_alter_label_sync_source.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-12-25 10:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('labels', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='label', + name='sync_source', + field=models.CharField(blank=True, default='', max_length=50, verbose_name='同步来源'), + ), + ] diff --git a/labels/migrations/__init__.py b/labels/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/labels/models.py b/labels/models.py new file mode 100644 index 0000000..1903e39 --- /dev/null +++ b/labels/models.py @@ -0,0 +1,309 @@ +"""Models for labels and related permissions.""" + +from django.db import models +from django.utils import timezone + + +class LabelType(models.TextChoices): + """标签类型枚举.""" + + PROJECT = "project", "项目" + ENTERPRISE = "enterprise", "企业" + FOUNDATION = "foundation", "基金会" + TECHNOLOGY = "technology", "技术领域" + COMMUNITY = "community", "社区" + + +class OwnerType(models.TextChoices): + """所有者类型枚举.""" + + SYSTEM = "system", "系统" # OpenDigger 同步的标签 + USER = "user", "个人" # 用户创建的标签 + ORGANIZATION = "organization", "组织" # 组织创建的标签 + + +class PermissionLevel(models.TextChoices): + """权限级别枚举.""" + + VIEW = "view", "查看" # 仅可查看标签及其数据 + USE = "use", "使用" # 可查看和使用标签(积分分发、筛选) + EDIT = "edit", "编辑" # 可查看、使用和编辑标签数据 + MANAGE = "manage", "管理" # 完全控制(包括删除、授权) + + +class GranteeType(models.TextChoices): + """被授权对象类型枚举.""" + + USER = "user", "用户" # 授权给个人用户 + ORGANIZATION = "organization", "组织" # 授权给整个组织 + + +class Label(models.Model): + """ + 标签模型, 用于分类项目、组织和开发者. + + 与 OpenDigger 标签系统集成. + """ + + name = models.CharField(max_length=200, db_index=True, verbose_name="英文名称") + name_zh = models.CharField(max_length=200, verbose_name="中文名称") + type = models.CharField( + max_length=20, + choices=LabelType.choices, + db_index=True, + verbose_name="标签类型", + ) + owner_type = models.CharField( + max_length=20, + choices=OwnerType.choices, + default=OwnerType.USER, + db_index=True, + verbose_name="所有者类型", + ) + owner_id = models.IntegerField( + null=True, blank=True, db_index=True, verbose_name="所有者ID" + ) + data = models.JSONField(default=dict, verbose_name="标签数据") + is_public = models.BooleanField(default=False, verbose_name="是否公开") + sync_source = models.CharField( + max_length=50, blank=True, default="", verbose_name="同步来源" + ) + last_synced_at = models.DateTimeField( + null=True, blank=True, verbose_name="最后同步时间" + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + """Meta options for Label.""" + + db_table = "labels" + verbose_name = "标签" + verbose_name_plural = "标签" + indexes = [ + models.Index(fields=["owner_type", "owner_id"]), + models.Index(fields=["type", "is_public"]), + ] + constraints = [ + models.UniqueConstraint( + fields=["name", "owner_type", "owner_id"], + name="unique_label_per_owner", + ) + ] + + def __str__(self): + """Return string representation with name and type.""" + return f"{self.name} ({self.get_type_display()})" + + def get_platform_entities(self, platform_name): + """提取特定平台的组织/仓库/开发者.""" + for platform in self.data.get("platforms", []): + if platform["name"] == platform_name: + return platform + return None + + def has_access(self, user, required_level="view"): + """ + 检查用户是否可以访问此标签. + + 支持通过 LabelPermission 的授权访问. + """ + allowed = False + + # 公开标签:允许查看/使用,不开放编辑或管理 + if self.is_public and required_level in ( + PermissionLevel.VIEW, + PermissionLevel.USE, + ): + allowed = True + + # 系统标签对所有人可见 + if not allowed and self.owner_type == OwnerType.SYSTEM: + allowed = required_level == "view" + + # 所有者拥有完全权限 + if ( + not allowed + and self.owner_type == OwnerType.USER + and self.owner_id == user.id + ): + allowed = True + + # 延迟导入以避免潜在循环依赖 + user_org_ids = set() + admin_org_ids = set() + if ( + self.owner_type == OwnerType.ORGANIZATION + or self.permissions.filter(grantee_type=GranteeType.ORGANIZATION).exists() + ): + from accounts.models import OrganizationMembership + + memberships = list( + OrganizationMembership.objects.filter(user=user).values( + "organization_id", "role" + ) + ) + user_org_ids = {m["organization_id"] for m in memberships} + admin_org_ids = { + m["organization_id"] + for m in memberships + if m["role"] + in ( + OrganizationMembership.Role.ADMIN, + OrganizationMembership.Role.OWNER, + ) + } + + if not allowed and self.owner_type == OwnerType.ORGANIZATION: + # 组织管理员/所有者拥有完全权限;普通成员可查看和使用 + if self.owner_id in admin_org_ids: + allowed = True + elif self.owner_id in user_org_ids and required_level in ( + PermissionLevel.VIEW, + PermissionLevel.USE, + ): + allowed = True + + # 检查直接授予用户的权限 + if not allowed: + user_permission = self.permissions.filter( + grantee_type=GranteeType.USER, grantee_id=user.id, is_active=True + ).first() + if user_permission and user_permission.check_permission(required_level): + allowed = True + + # 检查通过组织授予的权限 + if not allowed and user_org_ids: + org_permissions = self.permissions.filter( + grantee_type=GranteeType.ORGANIZATION, + grantee_id__in=user_org_ids, + is_active=True, + ) + for perm in org_permissions: + if perm.check_permission(required_level): + allowed = True + break + + return allowed + + def can_edit(self, user): + """检查用户是否可以编辑此标签.""" + if self.owner_type == OwnerType.SYSTEM: + return False + return self.has_access(user, required_level="edit") + + def can_manage(self, user): + """检查用户是否可以管理此标签(删除、授权等).""" + if self.owner_type == OwnerType.SYSTEM: + return False + return self.has_access(user, required_level="manage") + + +class LabelPermission(models.Model): + """标签共享权限模型, 支持跨用户和跨组织的标签访问控制.""" + + label = models.ForeignKey( + "Label", + on_delete=models.CASCADE, + related_name="permissions", + verbose_name="标签", + ) + grantee_type = models.CharField( + max_length=20, choices=GranteeType.choices, verbose_name="被授权对象类型" + ) + grantee_id = models.IntegerField(verbose_name="被授权对象ID") + permission_level = models.CharField( + max_length=20, choices=PermissionLevel.choices, verbose_name="权限级别" + ) + granted_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + related_name="granted_permissions", + verbose_name="授权者", + ) + granted_at = models.DateTimeField(auto_now_add=True, verbose_name="授权时间") + expires_at = models.DateTimeField(null=True, blank=True, verbose_name="过期时间") + is_active = models.BooleanField(default=True, verbose_name="是否激活") + notes = models.TextField(blank=True, verbose_name="备注") + + class Meta: + """Meta options for LabelPermission.""" + + db_table = "label_permissions" + verbose_name = "标签权限" + verbose_name_plural = "标签权限" + indexes = [ + models.Index(fields=["label", "grantee_type", "grantee_id"]), + models.Index(fields=["grantee_type", "grantee_id", "is_active"]), + models.Index(fields=["expires_at"]), + ] + constraints = [ + models.UniqueConstraint( + fields=["label", "grantee_type", "grantee_id"], + condition=models.Q(is_active=True), + name="unique_active_permission", + ) + ] + + def __str__(self): + """Return readable permission description.""" + return ( + f"{self.label.name} - {self.get_permission_level_display()} " + f"to {self.grantee_type}:{self.grantee_id}" + ) + + def is_expired(self): + """检查权限是否已过期.""" + if not self.expires_at: + return False + return timezone.now() > self.expires_at + + def check_permission(self, required_level): + """检查是否具有所需权限级别.""" + if not self.is_active or self.is_expired(): + return False + + # 权限级别层级: VIEW < USE < EDIT < MANAGE + levels = ["view", "use", "edit", "manage"] + current_index = levels.index(self.permission_level) + required_index = levels.index(required_level) + return current_index >= required_index + + +class LabelPermissionLog(models.Model): + """标签权限变更审计日志.""" + + ACTION_CHOICES = [ + ("granted", "授予"), + ("updated", "更新"), + ("revoked", "撤销"), + ("expired", "过期"), + ] + + permission = models.ForeignKey( + LabelPermission, on_delete=models.CASCADE, verbose_name="权限" + ) + action = models.CharField( + max_length=20, choices=ACTION_CHOICES, verbose_name="操作" + ) + actor = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + verbose_name="操作者", + ) + timestamp = models.DateTimeField(auto_now_add=True, verbose_name="时间戳") + details = models.JSONField(default=dict, verbose_name="变更详情") + + class Meta: + """Meta options for LabelPermissionLog.""" + + db_table = "label_permission_logs" + verbose_name = "标签权限日志" + verbose_name_plural = "标签权限日志" + ordering = ["-timestamp"] + + def __str__(self): + """Return string description of permission log entry.""" + return f"{self.get_action_display()} - {self.permission} at {self.timestamp}" diff --git a/labels/templatetags/__init__.py b/labels/templatetags/__init__.py new file mode 100644 index 0000000..3938725 --- /dev/null +++ b/labels/templatetags/__init__.py @@ -0,0 +1 @@ +"""Template tag package for labels app.""" diff --git a/labels/templatetags/json_tags.py b/labels/templatetags/json_tags.py new file mode 100644 index 0000000..23da584 --- /dev/null +++ b/labels/templatetags/json_tags.py @@ -0,0 +1,23 @@ +"""JSON template filters for labels.""" + +import json + +from django import template + +register = template.Library() + + +@register.filter(name="pretty_json") +def pretty_json(value): + """Format a dict or JSON string as pretty-printed JSON.""" + if value is None: + return "{}" + if isinstance(value, str): + try: + value = json.loads(value) + except (json.JSONDecodeError, TypeError): + return value + try: + return json.dumps(value, indent=2, ensure_ascii=False) + except (TypeError, ValueError): + return str(value) diff --git a/labels/tests/__init__.py b/labels/tests/__init__.py new file mode 100644 index 0000000..a1fbe6f --- /dev/null +++ b/labels/tests/__init__.py @@ -0,0 +1 @@ +# Tests for labels app diff --git a/labels/tests/test_access_control.py b/labels/tests/test_access_control.py new file mode 100644 index 0000000..101c8bc --- /dev/null +++ b/labels/tests/test_access_control.py @@ -0,0 +1,88 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from accounts.models import Organization, OrganizationMembership +from labels.models import ( + GranteeType, + Label, + LabelPermission, + LabelType, + OwnerType, + PermissionLevel, +) + + +class LabelAccessTests(TestCase): + """Tests covering Label.has_access permission logic.""" + + def setUp(self): + User = get_user_model() + self.owner = User.objects.create_user(username="owner", password="pass") + self.alice = User.objects.create_user(username="alice", password="pass") + self.bob = User.objects.create_user(username="bob", password="pass") + + self.org = Organization.objects.create(name="Org", slug="org") + OrganizationMembership.objects.create( + user=self.alice, + organization=self.org, + role=OrganizationMembership.Role.MEMBER, + ) + OrganizationMembership.objects.create( + user=self.bob, + organization=self.org, + role=OrganizationMembership.Role.ADMIN, + ) + + def test_public_label_allows_view_and_use(self): + label = Label.objects.create( + name="public-label", + name_zh="公开标签", + type=LabelType.PROJECT, + owner_type=OwnerType.USER, + owner_id=self.owner.id, + is_public=True, + ) + + self.assertTrue(label.has_access(self.alice, required_level="view")) + self.assertTrue(label.has_access(self.alice, required_level="use")) + self.assertFalse(label.has_access(self.alice, required_level="edit")) + + def test_org_owned_label_respects_membership_roles(self): + label = Label.objects.create( + name="org-label", + name_zh="组织标签", + type=LabelType.PROJECT, + owner_type=OwnerType.ORGANIZATION, + owner_id=self.org.id, + ) + + # Regular member: view/use allowed, edit/manage denied + self.assertTrue(label.has_access(self.alice, required_level="view")) + self.assertTrue(label.has_access(self.alice, required_level="use")) + self.assertFalse(label.has_access(self.alice, required_level="edit")) + self.assertFalse(label.has_access(self.alice, required_level="manage")) + + # Admin: full control + self.assertTrue(label.has_access(self.bob, required_level="edit")) + self.assertTrue(label.has_access(self.bob, required_level="manage")) + + def test_org_granted_permission_honors_membership(self): + label = Label.objects.create( + name="shared-to-org", + name_zh="共享标签", + type=LabelType.PROJECT, + owner_type=OwnerType.USER, + owner_id=self.owner.id, + ) + LabelPermission.objects.create( + label=label, + grantee_type=GranteeType.ORGANIZATION, + grantee_id=self.org.id, + permission_level=PermissionLevel.USE, + granted_by=self.owner, + ) + + # Member inherits org permission + self.assertTrue(label.has_access(self.alice, required_level="view")) + self.assertTrue(label.has_access(self.alice, required_level="use")) + self.assertFalse(label.has_access(self.alice, required_level="edit")) diff --git a/templates/accounts/label_form.html b/templates/accounts/label_form.html new file mode 100644 index 0000000..90b5d4a --- /dev/null +++ b/templates/accounts/label_form.html @@ -0,0 +1,86 @@ +{% extends "horizontal_base.html" %} +{% load json_tags %} + +{% block title %}{% if label %}编辑标签{% else %}创建标签{% endif %}{% endblock title %} + +{% block content %} +
+
+

{% if label %}编辑标签{% else %}创建标签{% endif %}

+
+ +
+
+
+
+
+
+ 标签信息 +
+
+ {% csrf_token %} +
+ + + {% if errors.name %} +
{{ errors.name }}
+ {% endif %} + 用于系统识别的英文标识 +
+
+ + + {% if errors.name_zh %} +
{{ errors.name_zh }}
+ {% endif %} + 用于显示的中文名称 +
+
+ + + {% if errors.type %} +
{{ errors.type }}
+ {% endif %} +
+
+
+ + +
+ 公开标签可被其他用户查看和使用 +
+
+ + + {% if errors.data %} +
{{ errors.data }}
+ {% endif %} + 用于存储标签相关的结构化数据,格式为 JSON +
+
+ 取消 + +
+
+
+
+
+
+{% endblock content %} diff --git a/templates/accounts/label_list.html b/templates/accounts/label_list.html new file mode 100644 index 0000000..06039ab --- /dev/null +++ b/templates/accounts/label_list.html @@ -0,0 +1,150 @@ +{% extends "horizontal_base.html" %} + +{% block title %}标签管理{% endblock title %} + +{% block content %} +
+
+

标签管理

+
+ +
+
+
+ +
+
+
+ 我的标签 +
+ {% if user_labels %} +
+ + + + + + + + + + + + {% for label in user_labels %} + + + + + + + + {% endfor %} + +
名称类型公开创建时间操作
+
+ {{ label.name_zh }} +
+ {{ label.name }} +
+
+ {{ label.get_type_display }} + + {% if label.is_public %} + + 公开 + + {% else %} + + 私有 + + {% endif %} + {{ label.created_at|date:"Y-m-d H:i" }} +
+ + + + + + +
+ {% csrf_token %} + +
+
+
+
+ {% else %} + + {% endif %} +
+
+
+ {% if shared_labels %} +
+ +
+
+
+ 共享给我的标签 +
+
+ + + + + + + + + + + {% for label in shared_labels %} + + + + + + + {% endfor %} + +
名称类型权限级别所有者
+
+ {{ label.name_zh }} +
+ {{ label.name }} +
+
+ {{ label.get_type_display }} + + + {% if label.permission_level == 'view' %} + 查看 + {% elif label.permission_level == 'use' %} + 使用 + {% elif label.permission_level == 'edit' %} + 编辑 + {% elif label.permission_level == 'manage' %} + 管理 + {% endif %} + + + + {{ label.get_owner_type_display }} + +
+
+
+
+
+ {% endif %} +
+{% endblock content %} diff --git a/templates/accounts/label_permission_grant.html b/templates/accounts/label_permission_grant.html new file mode 100644 index 0000000..7688768 --- /dev/null +++ b/templates/accounts/label_permission_grant.html @@ -0,0 +1,84 @@ +{% extends "horizontal_base.html" %} + +{% block title %}授予标签权限{% endblock title %} + +{% block content %} +
+
+

授予标签权限

+

标签: {{ label.name_zh }} ({{ label.name }})

+
+ +
+
+
+
+
+
+ 授权信息 +
+
+ {% csrf_token %} +
+ + + {% if errors.username %} +
{{ errors.username }}
+ {% endif %} + 输入要授予权限的用户的用户名 +
+
+ + + {% if errors.permission_level %} +
{{ errors.permission_level }}
+ {% endif %} +
+
+ + + {% if errors.expires_days %} +
{{ errors.expires_days }}
+ {% endif %} + 留空表示权限永久有效 +
+
+ + + 可选,用于说明授权原因 +
+ +
+ 取消 + +
+
+
+
+
+
+{% endblock content %} diff --git a/templates/accounts/label_permissions.html b/templates/accounts/label_permissions.html new file mode 100644 index 0000000..ca79e56 --- /dev/null +++ b/templates/accounts/label_permissions.html @@ -0,0 +1,154 @@ +{% extends "horizontal_base.html" %} + +{% block title %}标签权限管理{% endblock title %} + +{% block content %} +
+
+

标签权限管理

+

标签: {{ label.name_zh }} ({{ label.name }})

+
+ +
+
+
+
+
+
+ 权限列表 +
+ {% if permissions %} +
+ + + + + + + + + + + + + + {% for perm in permissions %} + + + + + + + + + + {% endfor %} + +
用户权限级别授予者授予时间过期时间备注操作
+ {% if perm.grantee_type == 'user' %} + + ID: {{ perm.grantee_id }} + + {% else %} + + 组织 ID: {{ perm.grantee_id }} + + {% endif %} + + {% if perm.permission_level == 'view' %} + + 查看 + + {% elif perm.permission_level == 'use' %} + + 使用 + + {% elif perm.permission_level == 'edit' %} + + 编辑 + + {% elif perm.permission_level == 'manage' %} + + 管理 + + {% endif %} + {{ perm.granted_by.username|default:"系统" }}{{ perm.granted_at|date:"Y-m-d H:i" }} + {% if perm.expires_at %} + {{ perm.expires_at|date:"Y-m-d H:i" }} + {% if perm.is_expired %} + 已过期 + {% endif %} + {% else %} + 永久 + {% endif %} + + {% if perm.notes %} + {{ perm.notes|truncatewords:10 }} + {% else %} + - + {% endif %} + +
+ {% csrf_token %} + +
+
+
+ {% else %} + + {% endif %} +
+
+
+
+ +
+
+
+
+
+ 权限级别说明 +
+
    +
  • + + 查看 + + - 仅可查看标签及其数据 +
  • +
  • + + 使用 + + - 可查看和使用标签(积分分发、筛选) +
  • +
  • + + 编辑 + + - 可查看、使用和编辑标签数据 +
  • +
  • + + 管理 + + - 完全控制(包括删除、授权) +
  • +
+
+
+
+
+{% endblock content %} diff --git a/templates/accounts/org_label_form.html b/templates/accounts/org_label_form.html new file mode 100644 index 0000000..2aede8d --- /dev/null +++ b/templates/accounts/org_label_form.html @@ -0,0 +1,86 @@ +{% extends "horizontal_base.html" %} +{% load json_tags %} + +{% block title %}{{ organization.name }} - {% if label %}编辑标签{% else %}创建标签{% endif %}{% endblock title %} + +{% block content %} +
+
+

{{ organization.name }} - {% if label %}编辑标签{% else %}创建标签{% endif %}

+
+ +
+
+
+
+
+
+ 标签信息 +
+
+ {% csrf_token %} +
+ + + {% if errors.name %} +
{{ errors.name }}
+ {% endif %} + 用于系统识别的英文标识 +
+
+ + + {% if errors.name_zh %} +
{{ errors.name_zh }}
+ {% endif %} + 用于显示的中文名称 +
+
+ + + {% if errors.type %} +
{{ errors.type }}
+ {% endif %} +
+
+
+ + +
+ 公开标签可被其他用户查看和使用 +
+
+ + + {% if errors.data %} +
{{ errors.data }}
+ {% endif %} + 用于存储标签相关的结构化数据,格式为 JSON +
+
+ 取消 + +
+
+
+
+
+
+{% endblock content %} diff --git a/templates/accounts/org_label_list.html b/templates/accounts/org_label_list.html new file mode 100644 index 0000000..80f2777 --- /dev/null +++ b/templates/accounts/org_label_list.html @@ -0,0 +1,104 @@ +{% extends "horizontal_base.html" %} + +{% block title %}{{ organization.name }} - 标签管理{% endblock title %} + +{% block content %} +
+
+

{{ organization.name }} - 标签管理

+
+
+ {% if is_admin %} + + 创建标签 + + {% endif %} + + 返回组织 + +
+
+
+
+
+
+
+ 组织标签 +
+ {% if org_labels %} +
+ + + + + + + + {% if is_admin %} + + {% endif %} + + + + {% for label in org_labels %} + + + + + + {% if is_admin %} + + {% endif %} + + {% endfor %} + +
名称类型公开创建时间操作
+
+ {{ label.name_zh }} +
+ {{ label.name }} +
+
+ {{ label.get_type_display }} + + {% if label.is_public %} + + 公开 + + {% else %} + + 私有 + + {% endif %} + {{ label.created_at|date:"Y-m-d H:i" }} +
+ + + +
+ {% csrf_token %} + +
+
+
+
+ {% else %} + + {% endif %} +
+
+
+
+{% endblock content %} diff --git a/templates/accounts/organization_detail.html b/templates/accounts/organization_detail.html index 01cea36..2e9100e 100644 --- a/templates/accounts/organization_detail.html +++ b/templates/accounts/organization_detail.html @@ -31,6 +31,9 @@

{{ organization.name }}

+ + {% if profile.work_experiences.all %}