diff --git a/apps/core/migrations/0058_remove_board_comment_access_mask_and_more.py b/apps/core/migrations/0058_remove_board_comment_access_mask_and_more.py new file mode 100644 index 00000000..31944323 --- /dev/null +++ b/apps/core/migrations/0058_remove_board_comment_access_mask_and_more.py @@ -0,0 +1,99 @@ +# Generated by Django 4.2.3 on 2024-01-10 17:16 + +import django.db.models.deletion +from django.db import migrations, models + +from apps.core.models import BoardAccessPermissionType + + +def move_board_permissions(apps, schema_editor): + Board = apps.get_model("core", "Board") + BoardPermission = apps.get_model("core", "BoardPermission") + Group = apps.get_model("user", "Group") + for board in Board.objects.all(): + read_access_mask = board.read_access_mask + write_access_mask = board.write_access_mask + comment_access_mask = board.comment_access_mask + + for group in Group.objects.all(): + read = bool((read_access_mask & (1 << (group.group_id - 1))) > 0) + write = bool((write_access_mask & (1 << (group.group_id - 1))) > 0) + comment = bool((comment_access_mask & (1 << (group.group_id - 1))) > 0) + + if read: + BoardPermission.objects.create( + board=board, + group=group, + permission=BoardAccessPermissionType.READ, + ) + if write: + BoardPermission.objects.create( + board=board, + group=group, + permission=BoardAccessPermissionType.WRITE, + ) + if comment: + BoardPermission.objects.create( + board=board, + group=group, + permission=BoardAccessPermissionType.COMMENT, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("user", "0022_group_remove_userprofile_group_and_more"), + ("core", "0057_alter_article_name_type_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="BoardPermission", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("permission", models.SmallIntegerField(verbose_name="permission")), + ( + "board", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.board", + verbose_name="board slug", + ), + ), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="user.group", + verbose_name="group", + ), + ), + ], + options={ + "verbose_name": "BoardPermission", + "verbose_name_plural": "BoardPermissions", + "unique_together": {("group", "board", "permission")}, + }, + ), + migrations.RunPython(move_board_permissions), + migrations.RemoveField( + model_name="board", + name="comment_access_mask", + ), + migrations.RemoveField( + model_name="board", + name="read_access_mask", + ), + migrations.RemoveField( + model_name="board", + name="write_access_mask", + ), + ] diff --git a/apps/core/models/__init__.py b/apps/core/models/__init__.py index 52eaaa10..fdc2cc6f 100644 --- a/apps/core/models/__init__.py +++ b/apps/core/models/__init__.py @@ -7,6 +7,7 @@ from .block import * from .board import * from .board_group import * +from .board_permission import * from .comment import * from .comment_log import * from .communication_article import * diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 4525bc4e..eb6f4ed1 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -21,7 +21,7 @@ ) from .block import Block -from .board import BoardAccessPermissionType, NameType +from .board import Board, NameType from .comment import Comment from .communication_article import SchoolResponseStatus from .report import Report @@ -111,7 +111,7 @@ class Article(MetaDataModel): db_index=True, default=None, ) - parent_board = models.ForeignKey( + parent_board: Board = models.ForeignKey( verbose_name="게시판", to="core.Board", on_delete=models.CASCADE, @@ -315,9 +315,7 @@ def hidden_reasons(self, user: User) -> list: if self.is_content_social and not user.profile.see_social: reasons.append(ArticleHiddenReason.SOCIAL_CONTENT) # 혹시 몰라 여기 두기는 하는데 여기 오기전에 Permission에서 막혀야 함 - if not self.parent_board.group_has_access_permission( - BoardAccessPermissionType.READ, user.profile.group - ): + if not self.parent_board.permission_list_by_user(user).READ: reasons.append(ArticleHiddenReason.ACCESS_DENIED_CONTENT) return reasons diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 45658b02..cc6c7009 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -1,10 +1,17 @@ -from enum import IntEnum, IntFlag, auto +from enum import IntFlag, auto from django.db import models from django_extensions.db.fields import AutoSlugField +from apps.user.models import Group from ara.db.models import MetaDataModel + from .board_group import BoardGroup +from .board_permission import ( + DEFAULT_PERMISSIONS, + BoardAccessPermission, + BoardPermission, +) class NameType(IntFlag): @@ -13,12 +20,6 @@ class NameType(IntFlag): REALNAME = auto() -class BoardAccessPermissionType(IntEnum): - READ = 0 - WRITE = 1 - COMMENT = 2 - - class Board(MetaDataModel): slug = AutoSlugField( populate_from=["en_name"], @@ -32,24 +33,6 @@ class Board(MetaDataModel): verbose_name="게시판 영문 이름", max_length=32, ) - # 사용자 그룹에 대해 접근 권한을 제어하는 bit mask 입니다. - # access_mask & (1 << user.group) > 0 일 때 접근이 가능합니다. - # 사용자 그룹의 값들은 `UserGroup`을 참고하세요. - read_access_mask = models.SmallIntegerField( - # UNAUTHORIZED, EXTERNAL_ORG 제외 모든 사용자 읽기 권한 부여 - verbose_name="읽기 권한", - default=0b011011110, - ) - write_access_mask = models.SmallIntegerField( - # UNAUTHORIZED, STORE_EMPLOYEE, EXTERNAL_ORG 제외 모든 사용자 쓰기 권한 부여 - verbose_name="쓰기 권한", - default=0b011011010, - ) - comment_access_mask = models.SmallIntegerField( - # UNAUTHORIZED 제외 모든 사용자 댓글 권한 부여 - verbose_name="댓글 권한", - default=0b011111110, - ) is_readonly = models.BooleanField( verbose_name="읽기 전용 게시판", default=False, @@ -113,18 +96,11 @@ class Meta(MetaDataModel.Meta): def __str__(self) -> str: return self.ko_name - def group_has_access_permission( - self, access_type: BoardAccessPermissionType, group: int - ) -> bool: - mask = None - if access_type == BoardAccessPermissionType.READ: - mask = self.read_access_mask - elif access_type == BoardAccessPermissionType.WRITE: - mask = self.write_access_mask - elif access_type == BoardAccessPermissionType.COMMENT: - mask = self.comment_access_mask - else: - # TODO: Handle error - return False + def permission_list_by_group(self, group: Group) -> BoardAccessPermission: + return BoardPermission.permission_list_by_group(group, self) + + def permission_list_by_user(self, user) -> BoardAccessPermission: + return BoardPermission.permission_list_by_user(user, self) - return (mask & (1 << group)) > 0 + def set_default_permission(self): + BoardPermission.add_permission_bulk_by_board(self, DEFAULT_PERMISSIONS) diff --git a/apps/core/models/board_permission.py b/apps/core/models/board_permission.py new file mode 100644 index 00000000..7cde03af --- /dev/null +++ b/apps/core/models/board_permission.py @@ -0,0 +1,203 @@ +from __future__ import annotations + +from enum import IntEnum +from typing import TYPE_CHECKING + +from django.conf import settings +from django.db import models + +from apps.user.models import Group, UserGroup, UserProfile + +if TYPE_CHECKING: + from .board import Board + + +class BoardAccessPermissionType(IntEnum): + DENY = 0 + READ = 1 + WRITE = 2 + COMMENT = 3 + DELETE = 4 + + +DEFAULT_READ_PERMISSION: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), +] +DEFAULT_WRITE_PERMISSION: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), +] +DEFAULT_COMMENT_PERMISSION: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.COMMENT), + (3, BoardAccessPermissionType.COMMENT), + (4, BoardAccessPermissionType.COMMENT), + (5, BoardAccessPermissionType.COMMENT), + (6, BoardAccessPermissionType.COMMENT), + (7, BoardAccessPermissionType.COMMENT), + (8, BoardAccessPermissionType.COMMENT), +] +DEFAULT_PERMISSIONS = [] +DEFAULT_PERMISSIONS.extend(DEFAULT_READ_PERMISSION) +DEFAULT_PERMISSIONS.extend(DEFAULT_WRITE_PERMISSION) +DEFAULT_PERMISSIONS.extend(DEFAULT_COMMENT_PERMISSION) + + +class BoardAccessPermission: + def __init__(self, target: UserProfile | Group, board: Board) -> None: + self.user = target if isinstance(target, UserProfile) else None + self.group = target if isinstance(target, Group) else None + self.board = board + self.READ = False + self.WRITE = False + self.COMMENT = False + self.DELETE = False + self.DENY = False + + def setPermission(self, permission: BoardAccessPermissionType) -> None: + if self.DENY: + return + + if permission == BoardAccessPermissionType.DENY: + self.DENY = True + self.READ = False + self.WRITE = False + self.COMMENT = False + self.DELETE = False + elif permission == BoardAccessPermissionType.READ: + self.READ = True + elif permission == BoardAccessPermissionType.WRITE: + self.WRITE = True + elif permission == BoardAccessPermissionType.COMMENT: + self.COMMENT = True + elif permission == BoardAccessPermissionType.DELETE: + self.DELETE = True + + +class BoardPermission(models.Model): + group = models.ForeignKey( + verbose_name="group", + to="user.Group", + on_delete=models.CASCADE, + db_index=True, + ) + board = models.ForeignKey( + verbose_name="board slug", + to="core.Board", + on_delete=models.CASCADE, + db_index=True, + ) + permission: int = models.SmallIntegerField( + verbose_name="permission", + null=False, + ) + + class Meta: + verbose_name = "BoardPermission" + verbose_name_plural = "BoardPermissions" + unique_together = (("group", "board", "permission"),) + + @staticmethod + def permission_list_by_group(group: Group, board: Board) -> BoardAccessPermission: + permissions = BoardAccessPermission(group, board) + groupPerms = BoardPermission.objects.filter(group=group, board=board) + for perm in groupPerms: + permissions.setPermission(perm.permission) + + return permissions + + @staticmethod + def permission_list_by_user( + user: settings.AUTH_USER_MODEL, board: Board + ) -> BoardAccessPermission: + groups = UserGroup.search_by_user(user) + permissions = BoardAccessPermission(user, board) + for group in groups: + groupPerms = BoardPermission.objects.filter(group=group, board=board) + for perm in groupPerms: + permissions.setPermission(perm.permission) + + return permissions + + @staticmethod + def add_permission( + group: Group, + board: Board, + permission: BoardAccessPermissionType, + ): + BoardPermission.objects.create( + group=group, + board=board, + permission=permission, + ) + + @staticmethod + def remove_permission( + group: Group, + board: Board, + permission: BoardAccessPermissionType, + ): + BoardPermission.objects.filter( + group=group, + board=board, + permission=permission, + ).delete() + + @staticmethod + def add_permission_bulk_by_board( + board: Board, + perms: list[tuple[int, BoardAccessPermissionType]], + ): + for group_id, perm in perms: + BoardPermission.objects.get_or_create( + group=Group.search_by_id(group_id), + board=board, + permission=perm, + ) + + @staticmethod + def set_group_permission(permission: BoardAccessPermission): + if permission.group is None: + # raise ValueError("permission.group is None") + return + BoardPermission.objects.filter( + group=permission.group, board=permission.board + ).delete() + if permission.DENY: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.DENY, + ) + return + if permission.READ: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.READ, + ) + if permission.WRITE: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.WRITE, + ) + if permission.COMMENT: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.COMMENT, + ) + if permission.DELETE: + BoardPermission.objects.create( + group=permission.group, + board=permission.board, + permission=BoardAccessPermissionType.DELETE, + ) diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index d67bd93c..e2ecdcb8 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -4,7 +4,6 @@ from apps.core.models import Comment, Notification from apps.core.models.communication_article import SchoolResponseStatus -from apps.user.models import UserProfile @receiver(models.signals.post_save, sender=Comment) @@ -33,8 +32,7 @@ def update_communication_article_status(comment): ) if ( article.parent_board.is_school_communication - and comment.created_by.profile.group - == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN + and comment.created_by.profile.is_school_admin and article.communication_article.school_response_status != SchoolResponseStatus.ANSWER_DONE ): diff --git a/apps/core/permissions/article.py b/apps/core/permissions/article.py index 588ddc51..a70001bb 100644 --- a/apps/core/permissions/article.py +++ b/apps/core/permissions/article.py @@ -1,7 +1,6 @@ from rest_framework import permissions from apps.core.models import Article -from apps.core.models.board import BoardAccessPermissionType class ArticlePermission(permissions.IsAuthenticated): @@ -15,6 +14,4 @@ class ArticleReadPermission(permissions.BasePermission): message = "해당 게시물에 대한 읽기 권한이 없습니다." def has_object_permission(self, request, view, obj: Article): - return obj.parent_board.group_has_access_permission( - BoardAccessPermissionType.READ, request.user.profile.group - ) + return obj.parent_board.permission_list_by_user(request.user).READ diff --git a/apps/core/permissions/communication_article.py b/apps/core/permissions/communication_article.py index 0d67bb3c..02862bb3 100644 --- a/apps/core/permissions/communication_article.py +++ b/apps/core/permissions/communication_article.py @@ -1,14 +1,8 @@ from rest_framework import permissions -from apps.user.models import UserProfile - class CommunicationArticleAdminPermission(permissions.IsAuthenticated): message = "You are not authorized to access this feature" def has_permission(self, request, view): - return ( - request.user.is_staff - or request.user.profile.group - == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN - ) + return request.user.is_staff or request.user.profile.is_school_admin diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 919af38c..d5289d59 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -6,7 +6,7 @@ from apps.core.documents import ArticleDocument from apps.core.models import Article, ArticleHiddenReason, Block, Board, Comment, Scrap -from apps.core.models.board import BoardAccessPermissionType, NameType +from apps.core.models.board import NameType from apps.core.serializers.attachment import AttachmentSerializer from apps.core.serializers.board import BoardSerializer from apps.core.serializers.mixins.hidden import ( @@ -523,9 +523,9 @@ def validate_parent_board(self, board: Board): user_is_superuser = self.context["request"].user.is_superuser if not user_is_superuser and board.is_readonly: raise serializers.ValidationError(gettext("This board is read only.")) - user_has_write_permission = board.group_has_access_permission( - BoardAccessPermissionType.WRITE, self.context["request"].user.profile.group - ) + user_has_write_permission = board.permission_list_by_user( + self.context["request"].user + ).WRITE if not user_has_write_permission: raise exceptions.PermissionDenied() return board diff --git a/apps/core/serializers/board.py b/apps/core/serializers/board.py index 7415e5fd..9f0f7c17 100644 --- a/apps/core/serializers/board.py +++ b/apps/core/serializers/board.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from apps.core.models.board import Board, BoardAccessPermissionType +from apps.core.models.board import Board from ara.classes.serializers import MetaDataModelSerializer @@ -51,14 +51,8 @@ class Meta(BaseBoardSerializer.Meta): "user_writable", ] - def get_user_readable(self, obj): - user = self.context["request"].user - return obj.group_has_access_permission( - BoardAccessPermissionType.READ, user.profile.group - ) + def get_user_readable(self, obj: Board): + return obj.permission_list_by_user(self.context["request"].user).READ - def get_user_writable(self, obj): - user = self.context["request"].user - return obj.group_has_access_permission( - BoardAccessPermissionType.WRITE, user.profile.group - ) + def get_user_writable(self, obj: Board): + return obj.permission_list_by_user(self.context["request"].user).WRITE diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 75f4eedb..1eb2f57f 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -11,7 +11,7 @@ from apps.core.filters.comment import CommentFilter from apps.core.models import Article, Comment, CommentDeleteLog, UserProfile, Vote -from apps.core.models.board import BoardAccessPermissionType, NameType +from apps.core.models.board import NameType from apps.core.permissions.comment import CommentPermission from apps.core.serializers.comment import ( CommentCreateActionSerializer, @@ -64,10 +64,7 @@ def create(self, request, *args, **kwargs): # self.check_object_permissions(request, parent_article) # Check permission - user_group = request.user.profile.group - if parent_article.parent_board.group_has_access_permission( - BoardAccessPermissionType.COMMENT, user_group - ): + if parent_article.parent_board.permission_list_by_user(request.user).COMMENT: return super().create(request, *args, **kwargs) return response.Response( {"message": gettext("Permission denied")}, status=status.HTTP_403_FORBIDDEN @@ -87,13 +84,10 @@ def perform_create(self, serializer): ) parent_article = parent_comment.parent_article - print(parent_article) + # print(parent_article) created_by = self.request.user - is_school_admin = ( - UserProfile.objects.get(user_id=created_by).group - == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN - ) + is_school_admin = UserProfile.objects.get(user_id=created_by).is_school_admin if is_school_admin and parent_article.name_type != NameType.ANONYMOUS: name_type = NameType.REGULAR diff --git a/apps/user/admin.py b/apps/user/admin.py index 185f7aaf..0ea863cd 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from apps.user.models import UserProfile +from apps.user.models import Group, UserGroup, UserProfile from apps.user.models.user.manual import ManualUser from ara.classes.admin import MetaDataModelAdmin @@ -10,7 +10,6 @@ class UserProfileAdmin(MetaDataModelAdmin): list_filter = ( "see_sexual", "see_social", - "group", "is_newara", ) list_display = ( @@ -18,7 +17,6 @@ class UserProfileAdmin(MetaDataModelAdmin): "sid", "nickname", "user", - "group", ) search_fields = ( "uid", @@ -37,3 +35,37 @@ class ManualUserAdmin(MetaDataModelAdmin): "applicant_name", "sso_email", ) + + +@admin.register(UserGroup) +class UserGroupAdmin(admin.ModelAdmin): + list_display = ( + "user", + "nickname", + "email", + "group", + ) + list_filter = ("group",) + search_fields = ( + "user__id", + "user__profile__nickname", + "user__email", + ) + + @admin.display(description="닉네임") + def nickname(self, obj: UserGroup): + return obj.user.profile.nickname + + @admin.display(description="이메일") + def email(self, obj: UserGroup): + return obj.user.email + + +@admin.register(Group) +class GroupAdmin(admin.ModelAdmin): + list_display = ( + "name", + "description", + "is_official", + ) + search_fields = ("name",) diff --git a/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py new file mode 100644 index 00000000..8b865e9d --- /dev/null +++ b/apps/user/migrations/0022_group_remove_userprofile_group_and_more.py @@ -0,0 +1,140 @@ +# Generated by Django 4.2.3 on 2024-01-10 17:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +def add_groups(apps, schema_editor): + default_groups = { + 1: ("Unauthorized user", "뉴아라 계정을 만들지 않은 사람들", False), + 2: ("KAIST member", "카이스트 메일을 가진 사람 (학생, 교직원)", False), + 3: ("Store employee", "교내 입주 업체 직원", True), + 4: ("Other member", "카이스트 메일이 없는 개인 (특수한 관련자 등)", False), + 5: ("KAIST organization", "교내 학생 단체들", True), + 6: ("External organization", "외부인 (홍보 계정 등)", False), + 7: ("Communication board admin", "소통게시판 관리인", False), + 8: ("News board admin", "뉴스게시판 관리인", False), + } + + Group = apps.get_model("user", "Group") + for group_id, (name, description, is_official) in default_groups.items(): + Group.objects.create( + group_id=group_id, + name=name, + description=description, + is_official=is_official, + ) + + +def add_user_groups(apps, schema_editor): + cursor = schema_editor.connection.cursor() + cursor.execute( + """ + INSERT INTO user_usergroup (user_id, group_id) + SELECT a.user_id, (a.group+1) FROM user_userprofile a, user_group b + WHERE (a.group+1) = b.group_id; + """ + ) + cursor.close() + + +def fix_manualuser_org_type(apps, schema_editor): + cursor = schema_editor.connection.cursor() + cursor.execute( + """ + UPDATE user_manualuser + SET org_type = org_type + 1; + """ + ) + cursor.close() + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("user", "0021_remove_userprofile_extra_preferences"), + ] + + operations = [ + migrations.CreateModel( + name="Group", + fields=[ + ( + "group_id", + models.AutoField( + db_index=True, + primary_key=True, + serialize=False, + verbose_name="Group ID", + ), + ), + ("name", models.CharField(max_length=32, verbose_name="Group name")), + ( + "description", + models.CharField( + max_length=128, null=True, verbose_name="Group description" + ), + ), + ( + "is_official", + models.BooleanField(default=False, verbose_name="공식 단체 또는 학생단체"), + ), + ], + options={ + "verbose_name": "Group", + "verbose_name_plural": "Groups", + }, + ), + migrations.CreateModel( + name="UserGroup", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="user.group", + verbose_name="그룹", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="사용자", + ), + ), + ], + options={ + "verbose_name": "사용자 그룹", + "verbose_name_plural": "사용자가 속한 그룹 목록", + "unique_together": {("user", "group")}, + }, + ), + migrations.RunPython(add_groups), + migrations.RunPython(add_user_groups), + migrations.RunPython(fix_manualuser_org_type), + migrations.RemoveField( + model_name="userprofile", + name="group", + ), + migrations.AlterField( + model_name="manualuser", + name="org_type", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="user.group", + verbose_name="업체/단체 그룹", + ), + ), + ] diff --git a/apps/user/models/__init__.py b/apps/user/models/__init__.py index a19f341f..de393058 100644 --- a/apps/user/models/__init__.py +++ b/apps/user/models/__init__.py @@ -1,3 +1,6 @@ from .fcm_token import * +from .group import * from .signals import * +from .user import * from .user_profile import * +from .usergroup import * diff --git a/apps/user/models/group.py b/apps/user/models/group.py new file mode 100644 index 00000000..53e8fc44 --- /dev/null +++ b/apps/user/models/group.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from django.db import models + + +class Group(models.Model): + group_id = models.AutoField( + verbose_name="Group ID", + primary_key=True, + db_index=True, + null=False, + ) + name = models.CharField( + verbose_name="Group name", + max_length=32, + null=False, + ) + description = models.CharField( + verbose_name="Group description", + max_length=128, + null=True, + ) + is_official = models.BooleanField( + verbose_name="공식 단체 또는 학생단체", + default=False, + ) + + class Meta: + verbose_name = "Group" + verbose_name_plural = "Groups" + + def __str__(self) -> str: + return self.name + + @staticmethod + def search_by_name(name: str) -> list[Group]: + return Group.objects.filter(name=name) + + @staticmethod + def search_by_id(group_id: int) -> Group: + return Group.objects.get(group_id=group_id) diff --git a/apps/user/models/user/manual.py b/apps/user/models/user/manual.py index 6183d644..0f930a6a 100644 --- a/apps/user/models/user/manual.py +++ b/apps/user/models/user/manual.py @@ -1,7 +1,7 @@ from django.conf import settings from django.db import models -from apps.user.models import UserProfile +from apps.user.models import Group from ara.db.models import MetaDataModel @@ -25,9 +25,10 @@ class Meta(MetaDataModel.Meta): verbose_name="업체/단체 이름", ) - org_type = models.IntegerField( - choices=UserProfile.UserGroup.choices, - default=UserProfile.UserGroup.UNAUTHORIZED, + org_type: Group = models.ForeignKey( + on_delete=models.CASCADE, + to="user.Group", + verbose_name="업체/단체 그룹", ) applicant_name = models.CharField( diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index ac1c1f19..3ea77b8d 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -5,11 +5,13 @@ from django.db import models from django.utils import timezone from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy +from apps.user.models import Group from ara.db.models import MetaDataModel from ara.settings import MIN_TIME +from .usergroup import UserGroup + class UserProfile(MetaDataModel): class Meta(MetaDataModel.Meta): @@ -21,26 +23,6 @@ class Meta(MetaDataModel.Meta): ("nickname", "is_newara", "deleted_at"), ) - class UserGroup(models.IntegerChoices): - # 뉴아라 계정을 만들지 않은 사람들 - UNAUTHORIZED = 0, gettext_lazy("Unauthorized user") - # 카이스트 메일을 가진 사람 (학생, 교직원) - KAIST_MEMBER = 1, gettext_lazy("KAIST member") - # 교내 입주 업체 직원 - STORE_EMPLOYEE = 2, gettext_lazy("Store employee") - # 카이스트 메일이 없는 개인 (특수한 관련자 등) - OTHER_MEMBER = 3, gettext_lazy("Other member") - # 교내 학생 단체들 - KAIST_ORG = 4, gettext_lazy("KAIST organization") - # 외부인 (홍보 계정 등) - EXTERNAL_ORG = 5, gettext_lazy("External organization") - # 소통게시판 관리인 - COMMUNICATION_BOARD_ADMIN = 6, gettext_lazy("Communication board admin") - # 뉴스게시판 관리인 - NEWS_BOARD_ADMIN = 7, gettext_lazy("News board admin") - - OFFICIAL_GROUPS = [UserGroup.STORE_EMPLOYEE, UserGroup.KAIST_ORG] - uid = models.CharField( null=True, default=None, @@ -85,10 +67,6 @@ class UserGroup(models.IntegerChoices): default=False, verbose_name="정치/사회성 보기", ) - group = models.IntegerField( - choices=UserGroup.choices, - default=UserGroup.UNAUTHORIZED, - ) user = models.OneToOneField( on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, @@ -124,6 +102,10 @@ def __str__(self) -> str: def can_change_nickname(self) -> bool: return (timezone.now() - relativedelta(months=3)) >= self.nickname_updated_at + @staticmethod + def get_by_auth_user(user: settings.AUTH_USER_MODEL) -> "UserProfile": + return UserProfile.objects.get(user=user) + @cached_property def email(self) -> str: return self.user.email @@ -140,11 +122,38 @@ def realname(self) -> str: @cached_property def is_official(self) -> bool: - return ( - self.group in UserProfile.OFFICIAL_GROUPS - or self.user.email == "new-ara@sparcs.org" - ) + if self.user.email == "new-ara@sparcs.org": + return True + return any(group.is_official for group in self.groups) @cached_property def is_school_admin(self) -> bool: - return self.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN + return self.has_group_by_id(7) # 7 = Communication board admin + + @cached_property + def groups(self) -> list[Group]: + return UserGroup.search_by_user(self.user) + + def has_group(self, group: Group) -> bool: + return group in self.groups + + def has_group_by_name(self, group_name: str) -> bool: + return any(group.name == group_name for group in self.groups) + + def has_group_by_id(self, group_id: int) -> bool: + return any(group.group_id == group_id for group in self.groups) + + def add_group(self, group: Group) -> None: + UserGroup.objects.get_or_create(user=self.user, group=group) + + def add_group_by_id(self, group_id: int) -> None: + group = Group.search_by_id(group_id) + self.add_group(group) + + def remove_group(self, group: Group) -> None: + if self.has_group(group): + UserGroup.objects.get(user=self.user, group=group).delete() + + def remove_group_by_id(self, group_id: int) -> None: + group = Group.search_by_id(group_id) + self.remove_group(group) diff --git a/apps/user/models/usergroup.py b/apps/user/models/usergroup.py new file mode 100644 index 00000000..c0f12107 --- /dev/null +++ b/apps/user/models/usergroup.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from django.conf import settings +from django.db import models + +from .group import Group + +if TYPE_CHECKING: + from apps.user.models import UserProfile + + +class UserGroup(models.Model): + user = models.ForeignKey( + verbose_name="사용자", + on_delete=models.CASCADE, + to=settings.AUTH_USER_MODEL, + db_index=True, + ) + + group = models.ForeignKey( + verbose_name="그룹", + on_delete=models.CASCADE, + to="user.Group", + db_index=True, + ) + + class Meta: + verbose_name = "사용자 그룹" + verbose_name_plural = "사용자가 속한 그룹 목록" + unique_together = (("user", "group"),) + + @staticmethod + def search_by_user(user: settings.AUTH_USER_MODEL) -> list[Group]: + return Group.objects.filter(usergroup__user=user).all() + + @staticmethod + def search_by_group(group: Group) -> list[UserProfile]: # WARNING: Too many results + return UserProfile.objects.filter(usergroup__group=group).all() diff --git a/apps/user/views/viewsets/user.py b/apps/user/views/viewsets/user.py index 0bbeef0d..f09ac60b 100644 --- a/apps/user/views/viewsets/user.py +++ b/apps/user/views/viewsets/user.py @@ -13,7 +13,7 @@ from django.utils.functional import cached_property from rest_framework import decorators, permissions, response, status -from apps.user.models import UserProfile +from apps.user.models import Group, UserGroup, UserProfile from apps.user.models.user.manual import ManualUser from apps.user.permissions.user import UserPermission from ara.classes.sparcssso import Client as SSOClient @@ -232,10 +232,12 @@ def sso_login_callback(self, request, *args, **kwargs): # 2. 아직 승인 이전, 회원가입을 시도했던 공용 계정 회원 if (not user_profile.user.is_active) and (is_kaist or is_manual): user_profile.user.is_active = True + if user_profile.has_group_by_id(1): # 1 = UNAUTHORIZED + user_profile.remove_group_by_id(1) if is_manual: - user_profile.group = manual_user.org_type + user_profile.add_group(manual_user.org_type) elif is_kaist: - user_profile.group = UserProfile.UserGroup.KAIST_MEMBER + user_profile.add_group_by_id(2) # 2 = KAIST_MEMBER user_profile.sso_user_info = user_info user_profile.save() @@ -258,7 +260,7 @@ def sso_login_callback(self, request, *args, **kwargs): password=str(uuid.uuid4()), is_active=is_kaist or is_manual, ) - user_group = UserProfile.UserGroup.UNAUTHORIZED + user_group = Group.search_by_id(1) # 1 = UNAUTHORIZED if is_manual: manual_user.user = new_user @@ -268,7 +270,7 @@ def sso_login_callback(self, request, *args, **kwargs): user_group = manual_user.org_type elif is_kaist: - user_group = UserProfile.UserGroup.KAIST_MEMBER + user_group = Group.search_by_id(2) # 2 = KAIST_MEMBER user_profile = UserProfile.objects.create( uid=user_info["uid"], @@ -276,10 +278,11 @@ def sso_login_callback(self, request, *args, **kwargs): nickname=user_nickname, sso_user_info=user_info, user=new_user, - group=user_group, picture=user_profile_picture, ) + UserGroup.objects.create(user=new_user, group=user_group) + if not user_profile.user.is_active: return redirect( to=reverse("core:InvalidSsoLoginView") diff --git a/tests/conftest.py b/tests/conftest.py index 7ab8a049..7404b650 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ from django.utils import timezone from rest_framework.test import APIClient -from apps.user.models import UserProfile +from apps.user.models import Group, UserGroup, UserProfile from ara import redis User = get_user_model() @@ -40,7 +40,6 @@ def set_user_client(request): UserProfile.objects.get_or_create( user=request.cls.user, nickname="User", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -48,6 +47,10 @@ def set_user_client(request): "last_name": "LastName", }, ) + UserGroup.objects.get_or_create( + user=request.cls.user, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) client = APIClient() request.cls.api_client = client @@ -61,7 +64,6 @@ def set_user_client2(request): UserProfile.objects.get_or_create( user=request.cls.user2, nickname="User2", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -69,6 +71,10 @@ def set_user_client2(request): "last_name": "LastName", }, ) + UserGroup.objects.get_or_create( + user=request.cls.user2, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -81,7 +87,6 @@ def set_user_client3(request): UserProfile.objects.get_or_create( user=request.cls.user3, nickname="User3", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -89,6 +94,10 @@ def set_user_client3(request): "last_name": "LastName", }, ) + UserGroup.objects.get_or_create( + user=request.cls.user3, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -102,7 +111,6 @@ def set_user_client4(request): UserProfile.objects.get_or_create( user=request.cls.user4, nickname="User4", - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -110,6 +118,10 @@ def set_user_client4(request): "last_name": "LastName", }, ) + UserGroup.objects.get_or_create( + user=request.cls.user4, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -124,9 +136,12 @@ def set_user_with_kaist_info(request): user=request.cls.user_with_kaist_info, nickname="user_with_kinfo", sso_user_info={"kaist_info": '{"ku_kname": "user_with_kaist_info"}'}, - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), ) + UserGroup.objects.get_or_create( + user=request.cls.user_with_kaist_info, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -145,9 +160,12 @@ def set_user_without_kaist_info(request): "last_name": "user_", "first_name": "without_kaist_info", }, - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), ) + UserGroup.objects.get_or_create( + user=request.cls.user_without_kaist_info, + group=Group.search_by_id(2), # 2 = KAIST_MEMBER + ) request.cls.api_client = APIClient() @@ -162,13 +180,16 @@ def set_school_admin(request): user=request.cls.school_admin, nickname="School Admin", agree_terms_of_service_at=timezone.now(), - group=UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN, sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', "first_name": "FirstName", "last_name": "LastName", }, ) + UserGroup.objects.get_or_create( + user=request.cls.school_admin, + group=Group.search_by_id(7), # 7 = Communication board admin + ) request.cls.api_client = APIClient() @@ -205,14 +226,15 @@ def create_user( username: str = "User", email: str = "user@sparcs.org", nickname: str = "Nickname", - group: UserProfile.UserGroup = UserProfile.UserGroup.KAIST_MEMBER, + group: int = 2 + # group: Group = Group.search_by_id(2), # 2 = KAIST_MEMBER ) -> User: + group = Group.search_by_id(2) user, created = User.objects.get_or_create(username=username, email=email) if created: UserProfile.objects.create( user=user, nickname=nickname, - group=group, agree_terms_of_service_at=timezone.now(), sso_user_info={ "kaist_info": '{"ku_kname": "\\ud669"}', @@ -220,22 +242,24 @@ def create_user( "last_name": f"Lastname", }, ) + UserGroup.objects.get_or_create(user=user, group=group) return user @classmethod - def create_user_with_index(cls, idx: int, group: UserProfile.UserGroup) -> User: + def create_user_with_index(cls, idx: int, group: Group) -> User: user = cls.create_user( username=f"User{idx}", email=f"user{idx}@sparcs.org", nickname=f"Nickname{idx}", - group=group, ) + UserGroup.objects.get_or_create(user=user, group=group) return user @classmethod def create_users( cls, num: int, - group: UserProfile.UserGroup = UserProfile.UserGroup.KAIST_MEMBER, + group: int = 2, # 2 = KAIST_MEMBER ) -> list[User]: + group = Group.search_by_id(2) return [cls.create_user_with_index(idx, group) for idx in range(num)] diff --git a/tests/test_articles.py b/tests/test_articles.py index f522df3a..28c8ebd9 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -4,8 +4,15 @@ from rest_framework import status from apps.core.models import Article, Block, Board, Comment, Topic, Vote -from apps.core.models.board import BoardAccessPermissionType, NameType -from apps.user.models import UserProfile +from apps.core.models.board import NameType +from apps.core.models.board_permission import ( + DEFAULT_COMMENT_PERMISSION, + DEFAULT_READ_PERMISSION, + DEFAULT_WRITE_PERMISSION, + BoardAccessPermissionType, + BoardPermission, +) +from apps.user.models import Group, UserGroup, UserProfile from ara.settings import MIN_TIME, SCHOOL_RESPONSE_VOTE_THRESHOLD from tests.conftest import RequestSetting, TestCase, Utils @@ -18,6 +25,7 @@ def set_boards(request): en_name="Test Board", name_type=NameType.REGULAR, ) + request.cls.board.set_default_permission() request.cls.anon_board = Board.objects.create( slug="anonymous", @@ -25,6 +33,7 @@ def set_boards(request): en_name="Anonymous", name_type=NameType.ANONYMOUS, ) + request.cls.anon_board.set_default_permission() request.cls.free_board = Board.objects.create( slug="free", @@ -32,6 +41,7 @@ def set_boards(request): en_name="Free", name_type=NameType.ANONYMOUS | NameType.REGULAR, ) + request.cls.free_board.set_default_permission() request.cls.realname_board = Board.objects.create( slug="test realname board", @@ -39,13 +49,31 @@ def set_boards(request): en_name="Test realname Board", name_type=NameType.REALNAME, ) + request.cls.realname_board.set_default_permission() request.cls.regular_access_board = Board.objects.create( slug="regular access", ko_name="일반 접근 권한 게시판", en_name="Regular Access Board", - read_access_mask=0b11011110, - write_access_mask=0b11011010, + # read_access_mask=0b11011110, + # write_access_mask=0b11011010, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.regular_access_board, permission_bulk ) # Though its name is 'advertiser accessible', enterprise is also accessible @@ -53,29 +81,74 @@ def set_boards(request): slug="advertiser accessible", ko_name="외부인(홍보 계정) 접근 가능 게시판", en_name="Advertiser Accessible Board", - read_access_mask=0b11111110, - write_access_mask=0b11111110, + # read_access_mask=0b11111110, + # write_access_mask=0b11111110, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (6, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.advertiser_accessible_board, permission_bulk ) request.cls.nonwritable_board = Board.objects.create( slug="nonwritable", ko_name="글 작성 불가 게시판", en_name="Nonwritable Board", - write_access_mask=0b00000000, + # write_access_mask=0b00000000, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.nonwritable_board, permission_bulk ) request.cls.newsadmin_writable_board = Board.objects.create( slug="newsadmin writable", ko_name="뉴스게시판 관리인 글 작성 가능 게시판", en_name="Newsadmin Writable Board", - write_access_mask=0b10000000, + # write_access_mask=0b10000000, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.newsadmin_writable_board, permission_bulk ) request.cls.enterprise_writable_board = Board.objects.create( slug="enterprise writable", ko_name="입주업체 글 작성 가능 게시판", en_name="Enterprise Writable Board", - write_access_mask=0b11011110, + # write_access_mask=0b11011110, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.WRITE), + (3, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.enterprise_writable_board, permission_bulk ) @@ -173,17 +246,29 @@ def set_kaist_articles(request): user=request.cls.kaist_user, nickname="KAIST User", agree_terms_of_service_at=timezone.now(), - group=UserProfile.UserGroup.KAIST_MEMBER, sso_user_info={}, ) + UserGroup.objects.get_or_create( + user=request.cls.kaist_user, + group=Group.search_by_id(2), # 2 = KAIST member + ) request.cls.kaist_board, _ = Board.objects.get_or_create( slug="kaist-only", ko_name="KAIST Board", en_name="KAIST Board", - read_access_mask=0b00000010, - write_access_mask=0b00000010, + # read_access_mask=0b00000010, + # write_access_mask=0b00000010, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.kaist_board, permission_bulk ) + request.cls.kaist_article, _ = Article.objects.get_or_create( title="example article", content="example content", @@ -328,10 +413,11 @@ def test_create(self): # get request 시 user의 read 권한 확인 테스트 def test_check_read_permission_when_get(self): group_users = [] - for idx, group in enumerate(UserProfile.UserGroup): - user = Utils.create_user_with_index(idx, group) + for group in Group.objects.all(): + user = Utils.create_user_with_index(group.group_id, group) group_users.append(user) - assert len(group_users) == len(UserProfile.UserGroup) + + assert len(group_users) == len(Group.objects.all()) articles = [self.regular_access_article, self.advertiser_accessible_article] @@ -339,9 +425,7 @@ def test_check_read_permission_when_get(self): for article in articles: res = self.http_request(user, "get", f"articles/{article.id}") - if article.parent_board.group_has_access_permission( - BoardAccessPermissionType.READ, user.profile.group - ): + if article.parent_board.permission_list_by_user(user).READ: assert res.status_code == status.HTTP_200_OK assert res.data["id"] == article.id else: @@ -350,10 +434,10 @@ def test_check_read_permission_when_get(self): # create 단계에서 user의 write 권한 확인 테스트 def test_check_write_permission_when_create(self): group_users = [] - for idx, group in enumerate(UserProfile.UserGroup): - user = Utils.create_user_with_index(idx, group) + for group in Group.objects.all(): + user = Utils.create_user_with_index(group.group_id, group) group_users.append(user) - assert len(group_users) == len(UserProfile.UserGroup) + assert len(group_users) == len(Group.objects.all()) boards = [ self.regular_access_board, @@ -378,9 +462,7 @@ def test_check_write_permission_when_create(self): }, ) - if board.group_has_access_permission( - BoardAccessPermissionType.WRITE, user.profile.group - ): + if board.permission_list_by_user(user).WRITE: assert res.status_code == status.HTTP_201_CREATED else: assert res.status_code == status.HTTP_403_FORBIDDEN @@ -697,6 +779,12 @@ def test_being_topped(self): """ THRESHOLD = 5 board = Board.objects.create(top_threshold=THRESHOLD) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [] + permission_bulk.extend(DEFAULT_READ_PERMISSION) + permission_bulk.extend(DEFAULT_WRITE_PERMISSION) + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board(board, permission_bulk) + article = Article.objects.create(created_by=self.user, parent_board=board) pk = article.pk @@ -908,10 +996,13 @@ def _user_factory(user_kwargs, profile_kwargs): **profile_kwargs, "user": user_instance, "agree_terms_of_service_at": timezone.now(), - "group": UserProfile.UserGroup.KAIST_MEMBER, "sso_user_info": {}, } ) + UserGroup.objects.get_or_create( + user=user_instance, + group=Group.search_by_id(2), # 2 = KAIST member + ) return user_instance @classmethod diff --git a/tests/test_block.py b/tests/test_block.py index 6684e870..835f3b61 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -16,6 +16,7 @@ def set_board(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") @@ -26,6 +27,7 @@ def set_anon_board(request): en_name="Anonymous", name_type=NameType.ANONYMOUS, ) + request.cls.anon_board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_comments.py b/tests/test_comments.py index 3daf6bca..3ae616c9 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -20,6 +20,7 @@ def set_boards(request): en_name="Test Board", name_type=NameType.REGULAR, ) + request.cls.board.set_default_permission() request.cls.realname_board = Board.objects.create( slug="test realname board", @@ -27,6 +28,7 @@ def set_boards(request): en_name="Test realname Board", name_type=NameType.REALNAME, ) + request.cls.realname_board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index ffbc4a1b..a4e1ec48 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -6,8 +6,13 @@ from django.utils import timezone from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST -from apps.core.models import Article, Board +from apps.core.models import Article, Board, BoardAccessPermissionType, BoardPermission from apps.core.models.board import NameType +from apps.core.models.board_permission import ( + DEFAULT_COMMENT_PERMISSION, + BoardAccessPermissionType, + BoardPermission, +) from apps.core.models.communication_article import ( CommunicationArticle, SchoolResponseStatus, @@ -26,8 +31,25 @@ def set_communication_board(request): en_name="With School (Test)", is_school_communication=True, name_type=NameType.REALNAME, - read_access_mask=0b11011110, - write_access_mask=0b11011010, + # read_access_mask=0b11011110, + # write_access_mask=0b11011010, + ) + permission_bulk: list[tuple[int, BoardAccessPermissionType]] = [ + (2, BoardAccessPermissionType.READ), + (3, BoardAccessPermissionType.READ), + (4, BoardAccessPermissionType.READ), + (5, BoardAccessPermissionType.READ), + (7, BoardAccessPermissionType.READ), + (8, BoardAccessPermissionType.READ), + (2, BoardAccessPermissionType.WRITE), + (4, BoardAccessPermissionType.WRITE), + (5, BoardAccessPermissionType.WRITE), + (7, BoardAccessPermissionType.WRITE), + (8, BoardAccessPermissionType.WRITE), + ] + permission_bulk.extend(DEFAULT_COMMENT_PERMISSION) + BoardPermission.add_permission_bulk_by_board( + request.cls.communication_board, permission_bulk ) @@ -39,6 +61,7 @@ def set_non_communication_board(request): en_name="Not With School (Test)", is_school_communication=False, ) + request.cls.non_communication_board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_recent.py b/tests/test_recent.py index 060ec588..a3044b28 100644 --- a/tests/test_recent.py +++ b/tests/test_recent.py @@ -17,6 +17,7 @@ def set_board(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_report.py b/tests/test_report.py index 3ff04750..5c1da8be 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -14,6 +14,7 @@ def set_board(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_scrap.py b/tests/test_scrap.py index 32146634..29946ac0 100644 --- a/tests/test_scrap.py +++ b/tests/test_scrap.py @@ -12,6 +12,7 @@ def set_board(request): ko_name="자유 게시판", en_name="Free Board", ) + request.cls.board.set_default_permission() @pytest.fixture(scope="class") diff --git a/tests/test_user.py b/tests/test_user.py index eddae31b..1a81b3a7 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -18,6 +18,7 @@ def set_boards(request): ko_name="테스트 게시판", en_name="Test Board", ) + request.cls.board.set_default_permission() request.cls.realname_board = Board.objects.create( slug="realname", @@ -25,6 +26,7 @@ def set_boards(request): en_name="Realname Board", name_type=NameType.REALNAME, ) + request.cls.realname_board.set_default_permission() @pytest.fixture(scope="class")