From 9552558cd2604e3d80c2b5c04652aa261ed21cc3 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Sat, 19 Feb 2022 18:08:16 +0900 Subject: [PATCH 001/101] Add github-actions --- .docker/codebuild.sh | 32 ------------ .github/workflows/generate-docker-tag.sh | 31 +++++++++++ .github/workflows/github-actions.yml | 65 ++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 32 deletions(-) delete mode 100644 .docker/codebuild.sh create mode 100644 .github/workflows/generate-docker-tag.sh create mode 100644 .github/workflows/github-actions.yml diff --git a/.docker/codebuild.sh b/.docker/codebuild.sh deleted file mode 100644 index aea678f6..00000000 --- a/.docker/codebuild.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - - -# CODEBUILD_WEBHOOK_TRIGGER: branch/, tag/, pr/ -if [ ! -z $CODEBUILD_WEBHOOK_TRIGGER ]; then - WEBHOOK_TYPE=$(echo $CODEBUILD_WEBHOOK_TRIGGER | cut -d '/' -f1) - NAME=$(echo $CODEBUILD_WEBHOOK_TRIGGER | cut -d '/' -f2-) - - if [ $WEBHOOK_TYPE = "branch" ]; then - export PUSH=true - if [ $NAME = "master" ]; then - export DOCKER_TAG=prod - export CACHE_DOCKER_TAG=prod - else - # Docker tag에 /가 들어갈 수 없어서 -로 변경 - export DOCKER_TAG=$(echo $NAME | sed -e "s/\//-/g") - export CACHE_DOCKER_TAG=dev - fi - elif [ $WEBHOOK_TYPE = "tag" ]; then - export PUSH=true - export DOCKER_TAG=$NAME - export CACHE_DOCKER_TAG=prod - else # pr - export PUSH=false - export CACHE_DOCKER_TAG=dev - fi -else # 직접 codebuild 실행 - export PUSH=false - export CACHE_DOCKER_TAG=dev -fi - -echo $WEBHOOK_TYPE $CACHE_DOCKER_TAG $DOCKER_TAG $PUSH diff --git a/.github/workflows/generate-docker-tag.sh b/.github/workflows/generate-docker-tag.sh new file mode 100644 index 00000000..02a0f72a --- /dev/null +++ b/.github/workflows/generate-docker-tag.sh @@ -0,0 +1,31 @@ +#!/bin/bash + + +# GITHUB_REF: refs/heads/, refs/tags/ +if [ ! -z $GITHUB_REF ]; then + TRIGGER_TYPE=$(echo $GITHUB_REF | cut -d '/' -f2) + NAME=$(echo $GITHUB_REF | cut -d '/' -f3) + + echo $GITHUB_REF + if [ $TRIGGER_TYPE = "heads" ]; then + export PUSH=true + if [ $NAME = "master" ]; then + export DOCKER_TAG=prod + export CACHE_DOCKER_TAG=prod + elif [ $NAME = "develop" ]; then + # Docker tag에 /가 들어갈 수 없어서 -로 변경 + export DOCKER_TAG=develop + export CACHE_DOCKER_TAG=develop + fi + elif [ $TRIGGER_TYPE = "tags" ]; then + export PUSH=true + export DOCKER_TAG=$NAME + export CACHE_DOCKER_TAG=prod + elif [ $TRIGGER_TYPE = "pull" ]; then + export PUSH=true + export DOCKER_TAG="PR$NAME" + export CACHE_DOCKER_TAG=develop + fi +fi + +echo $PUSH $TRIGGER_TYPE $CACHE_DOCKER_TAG $DOCKER_TAG diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 00000000..c0e9bd50 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,65 @@ +name: Run tests for new-ara-api +on: + workflow_dispatch: + push: + branches: + - master + - develop + pull_request: + release: + types: [created] +env: + ECR: 666583083672.dkr.ecr.ap-northeast-2.amazonaws.com + PROJECT_NAME: newara + SECRET_KEY: ${{ secrets.SECRET_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + SSO_CLIENT_ID: ${{ secrets.SSO_CLIENT_ID }} + SSO_SECRET_KEY: ${{ secrets.SSO_SECRET_KEY }} + PORTAL_2FA_KEY: ${{ secrets.PORTAL_2FA_KEY }} + AWS_REGION: ap-northeast-2 + ECR_REPOSITORY: newara + PASS_ALL_TESTS: false + +jobs: + deploy: + name: Run Tests + runs-on: ubuntu-18.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{env.AWS_REGION}} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Install + run: | + chmod +x .github/workflows/generate-docker-tag.sh + . .github/workflows/generate-docker-tag.sh + echo "PUSH=$PUSH" >> $GITHUB_ENV + echo "DOCKER_TAG=$DOCKER_TAG" >> $GITHUB_ENV + echo "CACHE_DOCKER_TAG=$CACHE_DOCKER_TAG" >> $GITHUB_ENV + docker pull $ECR/$PROJECT_NAME:$CACHE_DOCKER_TAG || true + - name: Build + run: | + docker build --build-arg AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID) --build-arg AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY) --cache-from $ECR/$PROJECT_NAME:$CACHE_DOCKER_TAG -t $PROJECT_NAME . + + - name: Run test + run: | + docker-compose -f docker-compose.test.yml run api test + docker-compose -f docker-compose.test.yml down + - if: env.PUSH == 'true' + name: Push docker image + run: | + echo "Start docker image push" + docker tag $PROJECT_NAME $ECR/$PROJECT_NAME:$DOCKER_TAG && docker push $ECR/$PROJECT_NAME:$DOCKER_TAG + echo "Finish docker image push" + \ No newline at end of file From f1b21cfdeef3b6e8f349dfbcdeb16de9c477b46f Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Sat, 26 Feb 2022 21:59:12 +0900 Subject: [PATCH 002/101] Add board_group field --- apps/core/migrations/0033_board_group_id.py | 18 +++++++++ apps/core/models/board.py | 5 +++ tmp.sql | 42 +++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 apps/core/migrations/0033_board_group_id.py create mode 100644 tmp.sql diff --git a/apps/core/migrations/0033_board_group_id.py b/apps/core/migrations/0033_board_group_id.py new file mode 100644 index 00000000..6158e131 --- /dev/null +++ b/apps/core/migrations/0033_board_group_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.9 on 2022-02-26 11:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0032_board_is_anonymous'), + ] + + operations = [ + migrations.AddField( + model_name='board', + name='group_id', + field=models.IntegerField(default=1, verbose_name='그룹 ID'), + ), + ] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index c088eda3..8057dcc4 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -61,6 +61,11 @@ class Meta(MetaDataModel.Meta): db_index=True ) + group_id = models.IntegerField( + verbose_name='그룹 ID', + default=1 + ) + def __str__(self) -> str: return self.ko_name diff --git a/tmp.sql b/tmp.sql new file mode 100644 index 00000000..85262981 --- /dev/null +++ b/tmp.sql @@ -0,0 +1,42 @@ +UPDATE new_ara.core_board t SET t.ko_name = '운영진공지', t.ko_description = '운영진공지' WHERE t.id = 8; +UPDATE new_ara.core_board t SET t.ko_name = '중고거래', t.ko_description = '중고거래' WHERE t.id = 4; +UPDATE new_ara.core_board t SET t.slug = 'facility-feedback', t.ko_name = '교내업체 피드백', t.en_name = 'Facility Feedback', t.ko_description = '교내업체 피드백', t.en_description = 'Facility Feedback' WHERE t.id = 5; + +INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, + en_description, is_readonly, access_mask, is_hidden, is_anonymous) +VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'facility-notice', + '교내 입주업체 공지', 'Facility Notice', '교내 입주업체 공지', 'Facility Notice', 0, 6, 0, 0); + + +INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, + en_description, is_readonly, access_mask, is_hidden, is_anonymous) +VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'club', '동아리', 'Club', + '동아리', 'Club', 0, 2, 0, 0); + +INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, + en_description, is_readonly, access_mask, is_hidden, is_anonymous) +VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'real-estate', '부동산', + 'Real Estate', '부동산', 'Real Estate', 0, 2, 0, 0); + +INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, + en_description, is_readonly, access_mask, is_hidden, is_anonymous) +VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'with-school', + '학교와의 게시판', 'With School', '학교와의 게시판', 'With School', 0, 2, 0, 0); + +INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, + en_description, is_readonly, access_mask, is_hidden, is_anonymous) +VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'with-newara', + '뉴아라팀과의 게시판', 'With Newara', '뉴아라팀과의 게시판', 'With Newara', 0, 2, 0, 0); + +-- after migration +UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 3; +UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 12; +UPDATE new_ara.core_board t SET t.group_id = 2 WHERE t.id = 6; +UPDATE new_ara.core_board t SET t.group_id = 2 WHERE t.id = 7; +UPDATE new_ara.core_board t SET t.group_id = 5 WHERE t.id = 5; +UPDATE new_ara.core_board t SET t.group_id = 5 WHERE t.id = 14; +UPDATE new_ara.core_board t SET t.group_id = 5 WHERE t.id = 13; +UPDATE new_ara.core_board t SET t.group_id = 3 WHERE t.id = 11 +UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 4; +UPDATE new_ara.core_board t SET t.group_id = 3 WHERE t.id = 2; +UPDATE new_ara.core_board t SET t.group_id = 2 WHERE t.id = 9; From 19f2d17f78b412628c20c7e4ecd2b10f62bd1ff8 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Mon, 28 Feb 2022 01:37:53 +0900 Subject: [PATCH 003/101] Add board_group table in sql --- tmp.sql | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tmp.sql b/tmp.sql index 85262981..644cf750 100644 --- a/tmp.sql +++ b/tmp.sql @@ -28,6 +28,23 @@ INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_nam VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'with-newara', '뉴아라팀과의 게시판', 'With Newara', '뉴아라팀과의 게시판', 'With Newara', 0, 2, 0, 0); +create table new_ara.core_board_group +( + id int auto_increment + primary key, + slug varchar(30) not null, + ko_name varchar(30) not null, + en_name varchar(30) not null +); + +INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (1, 'all', '모아보기', 'All'); +INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (2, 'notice', '공지', 'Notice'); +INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (3, 'chat', '잡담', 'Chat'); +INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (4, 'students', '학생 단체 및 동아리', 'Students'); +INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (5, 'money', '돈', 'Money'); +INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (6, 'communication', '소통', 'Communication'); + + -- after migration UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 3; UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 12; From 7b368911a057dd0f2d67b791abfa6ec559b45fc1 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Mon, 28 Feb 2022 12:34:55 +0900 Subject: [PATCH 004/101] Cleanup env var --- .github/workflows/generate-docker-tag.sh | 2 +- .github/workflows/github-actions.yml | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-docker-tag.sh b/.github/workflows/generate-docker-tag.sh index 02a0f72a..645a25d9 100644 --- a/.github/workflows/generate-docker-tag.sh +++ b/.github/workflows/generate-docker-tag.sh @@ -23,7 +23,7 @@ if [ ! -z $GITHUB_REF ]; then export CACHE_DOCKER_TAG=prod elif [ $TRIGGER_TYPE = "pull" ]; then export PUSH=true - export DOCKER_TAG="PR$NAME" + export DOCKER_TAG="pr$NAME" export CACHE_DOCKER_TAG=develop fi fi diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index c0e9bd50..54fd0e4b 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -18,8 +18,6 @@ env: SSO_SECRET_KEY: ${{ secrets.SSO_SECRET_KEY }} PORTAL_2FA_KEY: ${{ secrets.PORTAL_2FA_KEY }} AWS_REGION: ap-northeast-2 - ECR_REPOSITORY: newara - PASS_ALL_TESTS: false jobs: deploy: @@ -50,7 +48,7 @@ jobs: docker pull $ECR/$PROJECT_NAME:$CACHE_DOCKER_TAG || true - name: Build run: | - docker build --build-arg AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID) --build-arg AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY) --cache-from $ECR/$PROJECT_NAME:$CACHE_DOCKER_TAG -t $PROJECT_NAME . + docker build --build-arg AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID --build-arg AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY) --cache-from $ECR/$PROJECT_NAME:$CACHE_DOCKER_TAG -t $PROJECT_NAME . - name: Run test run: | @@ -62,4 +60,3 @@ jobs: echo "Start docker image push" docker tag $PROJECT_NAME $ECR/$PROJECT_NAME:$DOCKER_TAG && docker push $ECR/$PROJECT_NAME:$DOCKER_TAG echo "Finish docker image push" - \ No newline at end of file From ba197b547f65144d65f55173ce2d82ffcf3b3d5f Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Mon, 28 Feb 2022 19:08:15 +0900 Subject: [PATCH 005/101] Delete tmp.sql --- tmp.sql | 59 --------------------------------------------------------- 1 file changed, 59 deletions(-) delete mode 100644 tmp.sql diff --git a/tmp.sql b/tmp.sql deleted file mode 100644 index 644cf750..00000000 --- a/tmp.sql +++ /dev/null @@ -1,59 +0,0 @@ -UPDATE new_ara.core_board t SET t.ko_name = '운영진공지', t.ko_description = '운영진공지' WHERE t.id = 8; -UPDATE new_ara.core_board t SET t.ko_name = '중고거래', t.ko_description = '중고거래' WHERE t.id = 4; -UPDATE new_ara.core_board t SET t.slug = 'facility-feedback', t.ko_name = '교내업체 피드백', t.en_name = 'Facility Feedback', t.ko_description = '교내업체 피드백', t.en_description = 'Facility Feedback' WHERE t.id = 5; - -INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, - en_description, is_readonly, access_mask, is_hidden, is_anonymous) -VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'facility-notice', - '교내 입주업체 공지', 'Facility Notice', '교내 입주업체 공지', 'Facility Notice', 0, 6, 0, 0); - - -INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, - en_description, is_readonly, access_mask, is_hidden, is_anonymous) -VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'club', '동아리', 'Club', - '동아리', 'Club', 0, 2, 0, 0); - -INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, - en_description, is_readonly, access_mask, is_hidden, is_anonymous) -VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'real-estate', '부동산', - 'Real Estate', '부동산', 'Real Estate', 0, 2, 0, 0); - -INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, - en_description, is_readonly, access_mask, is_hidden, is_anonymous) -VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'with-school', - '학교와의 게시판', 'With School', '학교와의 게시판', 'With School', 0, 2, 0, 0); - -INSERT INTO new_ara.core_board (created_at, updated_at, deleted_at, slug, ko_name, en_name, ko_description, - en_description, is_readonly, access_mask, is_hidden, is_anonymous) -VALUES ('2022-02-24 21:41:37.000000', '2022-02-24 21:41:39.000000', '0001-01-01 00:00:00.000000', 'with-newara', - '뉴아라팀과의 게시판', 'With Newara', '뉴아라팀과의 게시판', 'With Newara', 0, 2, 0, 0); - -create table new_ara.core_board_group -( - id int auto_increment - primary key, - slug varchar(30) not null, - ko_name varchar(30) not null, - en_name varchar(30) not null -); - -INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (1, 'all', '모아보기', 'All'); -INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (2, 'notice', '공지', 'Notice'); -INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (3, 'chat', '잡담', 'Chat'); -INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (4, 'students', '학생 단체 및 동아리', 'Students'); -INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (5, 'money', '돈', 'Money'); -INSERT INTO new_ara.core_board_group (id, slug, ko_name, en_name) VALUES (6, 'communication', '소통', 'Communication'); - - --- after migration -UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 3; -UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 12; -UPDATE new_ara.core_board t SET t.group_id = 2 WHERE t.id = 6; -UPDATE new_ara.core_board t SET t.group_id = 2 WHERE t.id = 7; -UPDATE new_ara.core_board t SET t.group_id = 5 WHERE t.id = 5; -UPDATE new_ara.core_board t SET t.group_id = 5 WHERE t.id = 14; -UPDATE new_ara.core_board t SET t.group_id = 5 WHERE t.id = 13; -UPDATE new_ara.core_board t SET t.group_id = 3 WHERE t.id = 11 -UPDATE new_ara.core_board t SET t.group_id = 4 WHERE t.id = 4; -UPDATE new_ara.core_board t SET t.group_id = 3 WHERE t.id = 2; -UPDATE new_ara.core_board t SET t.group_id = 2 WHERE t.id = 9; From d611adab113ea07206cf1492fdf64c770579db81 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Wed, 9 Mar 2022 23:44:01 +0900 Subject: [PATCH 006/101] Add board's banner image and description --- .../migrations/0034_board_banner_image.py | 18 +++++++++++++++ .../migrations/0035_auto_20220309_2342.py | 23 +++++++++++++++++++ apps/core/models/board.py | 22 ++++++++++++++++++ apps/core/serializers/board.py | 2 -- 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 apps/core/migrations/0034_board_banner_image.py create mode 100644 apps/core/migrations/0035_auto_20220309_2342.py diff --git a/apps/core/migrations/0034_board_banner_image.py b/apps/core/migrations/0034_board_banner_image.py new file mode 100644 index 00000000..10a039bb --- /dev/null +++ b/apps/core/migrations/0034_board_banner_image.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-03-09 14:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0033_board_group_id'), + ] + + operations = [ + migrations.AddField( + model_name='board', + name='banner_image', + field=models.ImageField(blank=True, default=None, null=True, upload_to='board/banner_images', verbose_name='게시판 배너 이미지'), + ), + ] diff --git a/apps/core/migrations/0035_auto_20220309_2342.py b/apps/core/migrations/0035_auto_20220309_2342.py new file mode 100644 index 00000000..c66a646f --- /dev/null +++ b/apps/core/migrations/0035_auto_20220309_2342.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2 on 2022-03-09 14:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0034_board_banner_image'), + ] + + operations = [ + migrations.AddField( + model_name='board', + name='en_banner_description', + field=models.TextField(blank=True, default=None, null=True, verbose_name='게시판 배너에 삽입되는 영문 소개'), + ), + migrations.AddField( + model_name='board', + name='ko_banner_description', + field=models.TextField(blank=True, default=None, null=True, verbose_name='게시판 배너에 삽입되는 국문 소개'), + ), + ] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 8057dcc4..6dcd19fe 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -66,6 +66,28 @@ class Meta(MetaDataModel.Meta): default=1 ) + banner_image = models.ImageField( + null=True, + blank=True, + default=None, + upload_to='board/banner_images', + verbose_name='게시판 배너 이미지', + ) + + ko_banner_description = models.TextField( + null=True, + blank=True, + default=None, + verbose_name='게시판 배너에 삽입되는 국문 소개', + ) + + en_banner_description = models.TextField( + null=True, + blank=True, + default=None, + verbose_name='게시판 배너에 삽입되는 영문 소개', + ) + def __str__(self) -> str: return self.ko_name diff --git a/apps/core/serializers/board.py b/apps/core/serializers/board.py index 125c30ae..9e7336f7 100644 --- a/apps/core/serializers/board.py +++ b/apps/core/serializers/board.py @@ -1,5 +1,3 @@ -from rest_framework import serializers - from ara.classes.serializers import MetaDataModelSerializer from apps.core.models import Board From f327df84a855f06a34944fca33a7990a7d39cd77 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Wed, 9 Mar 2022 23:55:38 +0900 Subject: [PATCH 007/101] Add default board_banner_image --- .../0036_alter_board_banner_image.py | 18 ++++++++++++++++++ apps/core/models/board.py | 6 ++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 apps/core/migrations/0036_alter_board_banner_image.py diff --git a/apps/core/migrations/0036_alter_board_banner_image.py b/apps/core/migrations/0036_alter_board_banner_image.py new file mode 100644 index 00000000..4f88e0ab --- /dev/null +++ b/apps/core/migrations/0036_alter_board_banner_image.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-03-09 14:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0035_auto_20220309_2342'), + ] + + operations = [ + migrations.AlterField( + model_name='board', + name='banner_image', + field=models.ImageField(default='default_banner.png', upload_to='board_banner_images', verbose_name='게시판 배너 이미지'), + ), + ] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 6dcd19fe..11d2756f 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -67,10 +67,8 @@ class Meta(MetaDataModel.Meta): ) banner_image = models.ImageField( - null=True, - blank=True, - default=None, - upload_to='board/banner_images', + default='default_banner.png', + upload_to='board_banner_images', verbose_name='게시판 배너 이미지', ) From ecf8499771a29117e7c0f93f9bfefb2d372ad9e3 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Tue, 15 Mar 2022 00:53:31 +0900 Subject: [PATCH 008/101] Add board_banner_url --- ...342.py => 0035_board_banner_description.py} | 0 .../0036_alter_board_banner_image.py | 2 +- apps/core/migrations/0037_board_banner_url.py | 18 ++++++++++++++++++ apps/core/models/board.py | 7 +++++++ 4 files changed, 26 insertions(+), 1 deletion(-) rename apps/core/migrations/{0035_auto_20220309_2342.py => 0035_board_banner_description.py} (100%) create mode 100644 apps/core/migrations/0037_board_banner_url.py diff --git a/apps/core/migrations/0035_auto_20220309_2342.py b/apps/core/migrations/0035_board_banner_description.py similarity index 100% rename from apps/core/migrations/0035_auto_20220309_2342.py rename to apps/core/migrations/0035_board_banner_description.py diff --git a/apps/core/migrations/0036_alter_board_banner_image.py b/apps/core/migrations/0036_alter_board_banner_image.py index 4f88e0ab..a9439d13 100644 --- a/apps/core/migrations/0036_alter_board_banner_image.py +++ b/apps/core/migrations/0036_alter_board_banner_image.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0035_auto_20220309_2342'), + ('core', '0035_board_banner_description'), ] operations = [ diff --git a/apps/core/migrations/0037_board_banner_url.py b/apps/core/migrations/0037_board_banner_url.py new file mode 100644 index 00000000..dc6aee84 --- /dev/null +++ b/apps/core/migrations/0037_board_banner_url.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.9 on 2022-03-14 15:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0036_alter_board_banner_image'), + ] + + operations = [ + migrations.AddField( + model_name='board', + name='banner_url', + field=models.TextField(blank=True, default=None, null=True, verbose_name='게시판 배너를 클릭 시에 이동하는 링크'), + ), + ] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 11d2756f..9e7efbee 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -86,6 +86,13 @@ class Meta(MetaDataModel.Meta): verbose_name='게시판 배너에 삽입되는 영문 소개', ) + banner_url = models.TextField( + null=True, + blank=True, + default=None, + verbose_name='게시판 배너를 클릭 시에 이동하는 링크', + ) + def __str__(self) -> str: return self.ko_name From abc6947dd53532aabecad78a32b9bc4a5d419994 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 21 Mar 2022 23:01:11 +0900 Subject: [PATCH 009/101] Add is_official & is_school_admin to user profile --- ...017_add_is-official_and_is-school-admin.py | 23 +++++++++++++++++++ apps/user/models/user_profile.py | 10 ++++++++ apps/user/serializers/user_profile.py | 4 +++- 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 apps/user/migrations/0017_add_is-official_and_is-school-admin.py diff --git a/apps/user/migrations/0017_add_is-official_and_is-school-admin.py b/apps/user/migrations/0017_add_is-official_and_is-school-admin.py new file mode 100644 index 00000000..e9fdc642 --- /dev/null +++ b/apps/user/migrations/0017_add_is-official_and_is-school-admin.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.9 on 2022-03-21 13:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0016_set_created_at_to_timezone_now'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='is_official', + field=models.BooleanField(default=False, verbose_name='공식 계정'), + ), + migrations.AddField( + model_name='userprofile', + name='is_school_admin', + field=models.BooleanField(default=False, verbose_name='학교 관리자'), + ), + ] diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index ead98e37..6ced5aa8 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -112,6 +112,16 @@ class UserGroup(models.IntegerChoices): verbose_name='활동정지 마감 일시', ) + is_official = models.BooleanField( + default=False, + verbose_name='공식 계정', + ) + + is_school_admin = models.BooleanField( + default=False, + verbose_name='학교 관리자', + ) + def __str__(self) -> str: return self.nickname diff --git a/apps/user/serializers/user_profile.py b/apps/user/serializers/user_profile.py index ee133ca5..428a7838 100644 --- a/apps/user/serializers/user_profile.py +++ b/apps/user/serializers/user_profile.py @@ -60,7 +60,9 @@ class Meta(BaseUserProfileSerializer.Meta): fields = ( 'picture', 'nickname', - 'user' + 'user', + 'is_official', + 'is_school_admin', ) From c11ed5b9b63e5534840439687bccc5d5939425b0 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Thu, 24 Mar 2022 23:51:29 +0900 Subject: [PATCH 010/101] Change is_anonymous field to smallint, implement user_realname in postprocess_created_by --- apps/core/models/article.py | 20 +++++++++++++++++--- apps/core/models/board.py | 4 ++-- apps/core/serializers/article.py | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 642c22d3..578f6d5e 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -2,6 +2,7 @@ import typing from enum import Enum from typing import Dict, Union +import json from django.core.files.storage import default_storage @@ -46,7 +47,7 @@ class Meta(MetaDataModel.Meta): verbose_name='text 형식 본문', ) - is_anonymous = models.BooleanField( + is_anonymous = models.SmallIntegerField( default=False, verbose_name='익명', ) @@ -206,9 +207,9 @@ def created_by_nickname(self): # API 상에서 보이는 사용자 (익명일 경우 익명화된 글쓴이, 그 외는 그냥 글쓴이) @cached_property def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: - if not self.is_anonymous: + if self.is_anonymous == 0: return self.created_by - else: + elif self.is_anonymous == 1: user_unique_num = self.created_by.id + self.id + HASH_SECRET_VALUE user_unique_encoding = str(hex(user_unique_num)).encode('utf-8') user_hash = hashlib.sha224(user_unique_encoding).hexdigest() @@ -224,6 +225,19 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'user': gettext('anonymous') }, } + else: + sso_info = self.created_by.profile.sso_user_info + user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] + user_profile_picture = make_random_profile_picture() + return { + 'id': 0, + 'username': user_realname, + 'profile': { + 'picture': default_storage.url(user_profile_picture), + 'nickname': user_realname, + 'user': user_realname + }, + } @cache_by_user def hidden_reasons(self, user: settings.AUTH_USER_MODEL) -> typing.List: diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 9e7efbee..21109481 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -54,10 +54,10 @@ class Meta(MetaDataModel.Meta): db_index=True, ) - is_anonymous = models.BooleanField( + is_anonymous = models.SmallIntegerField( verbose_name='익명 게시판', help_text='게시판의 글과 댓글들이 익명이도록 합니다.', - default=False, + default=0, db_index=True ) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 3fa2e92b..ddf8d8da 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -57,7 +57,7 @@ def get_content(self, obj) -> typing.Optional[str]: return None def get_created_by(self, obj) -> dict: - if obj.is_anonymous: + if obj.is_anonymous == 1 or obj.is_anonymous == 2: return obj.postprocessed_created_by else: data = PublicUserSerializer(obj.postprocessed_created_by).data From 2ec5ff0abbfa6742b1572171e1cd0d75f866846f Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 23 Mar 2022 00:02:06 +0900 Subject: [PATCH 011/101] Create CommunicationArticle model --- apps/core/models/communication_article.py | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 apps/core/models/communication_article.py diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py new file mode 100644 index 00000000..500163a3 --- /dev/null +++ b/apps/core/models/communication_article.py @@ -0,0 +1,35 @@ +from django.db import models + +from ara.db.models import MetaDataModel + + +class CommunicationArticle(MetaDataModel): + class Meta(MetaDataModel.Meta): + verbose_name = '소통 게시물' + verbose_name_plural = '소통 게시물 목록' + + article_id = models.OneToOneField( + to='core.Article', + on_delete=models.CASCADE, + db_index=True, + verbose_name='게시물', + ) + + need_answer = models.BooleanField( + default=True, + verbose_name='답변 요청 여부', + ) + response_deadline = models.DateTimeField( + null=True, + default=None, + verbose_name='답변 요청 기한', + ) + + is_checked_by_school = models.BooleanField( + default=False, + verbose_name='학교 답변 여부', + ) + is_answered = models.Booleanfield( + default=False, + verbose_name='답변 완료', + ) From 036be94aecda5efcc69ae6b9af627a0617fe9a59 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 23 Mar 2022 00:03:01 +0900 Subject: [PATCH 012/101] Create CommunicationArticleSerializer --- apps/core/serializers/communication_article.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 apps/core/serializers/communication_article.py diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py new file mode 100644 index 00000000..979c565a --- /dev/null +++ b/apps/core/serializers/communication_article.py @@ -0,0 +1,13 @@ +from ara.classes.serializers import MetaDataModelSerializer + +from apps.core.models.communication_article import CommunicationArticle + + +class BaseCommunicationArticleSerializer(MetaDataModelSerializer): + class Meta: + model = CommunicationArticle + fields = '__all__' + + +class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): + pass From ba44d9f4e7865bd1c01b7c2a2ad9f426ce43668f Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 23 Mar 2022 00:39:03 +0900 Subject: [PATCH 013/101] Add communication_article to __init__.py --- apps/core/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/core/models/__init__.py b/apps/core/models/__init__.py index e5954f2d..5f019bec 100644 --- a/apps/core/models/__init__.py +++ b/apps/core/models/__init__.py @@ -15,5 +15,5 @@ from .block import * from .scrap import * from .faq import * - +from .communication_article import * from .signals import * From 127af2ee5579e942f0c0d0587504ba662d86ea9b Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Thu, 24 Mar 2022 17:07:23 +0900 Subject: [PATCH 014/101] Fix communication_article boolean fields to datetime --- apps/core/models/communication_article.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index 500163a3..94360319 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils import timezone from ara.db.models import MetaDataModel @@ -15,21 +16,18 @@ class Meta(MetaDataModel.Meta): verbose_name='게시물', ) - need_answer = models.BooleanField( - default=True, - verbose_name='답변 요청 여부', - ) response_deadline = models.DateTimeField( null=True, - default=None, + default=timezone.datetime.min.replace(tzinfo=timezone.utc), verbose_name='답변 요청 기한', ) - is_checked_by_school = models.BooleanField( - default=False, - verbose_name='학교 답변 여부', + confirmed_by_school_at = models.BooleanField( + default=timezone.datetime.min.replace(tzinfo=timezone.utc), + verbose_name='학교측 문의 확인 시각', ) - is_answered = models.Booleanfield( - default=False, - verbose_name='답변 완료', + + answered_at = models.BooleanField( + default=timezone.datetime.min.replace(tzinfo=timezone.utc), + verbose_name='학교측 답변을 받은 시각', ) From 4f43ccf9187fba3436b2aedba5d38dad6015d352 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Mon, 28 Mar 2022 20:46:19 +0900 Subject: [PATCH 015/101] Add model for communication article --- .../migrations/0038_communicationarticle.py | 36 +++++++++++++++++++ apps/core/models/communication_article.py | 17 ++++++--- apps/core/serializers/article.py | 21 +++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 apps/core/migrations/0038_communicationarticle.py diff --git a/apps/core/migrations/0038_communicationarticle.py b/apps/core/migrations/0038_communicationarticle.py new file mode 100644 index 00000000..2741e53d --- /dev/null +++ b/apps/core/migrations/0038_communicationarticle.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.9 on 2022-03-28 11:45 + +import datetime +from django.db import migrations, models +import django.db.models.deletion +from django.utils.timezone import utc +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0037_board_banner_url'), + ] + + operations = [ + migrations.CreateModel( + name='CommunicationArticle', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='생성 시간')), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True, verbose_name='수정 시간')), + ('deleted_at', models.DateTimeField(db_index=True, default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='삭제 시간')), + ('response_deadline', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='답변 요청 기한')), + ('confirmed_by_school_at', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='학교측 문의 확인 시각')), + ('answered_at', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='학교측 답변을 받은 시각')), + ('article', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='communication_article', to='core.article', verbose_name='게시물')), + ], + options={ + 'verbose_name': '소통 게시물', + 'verbose_name_plural': '소통 게시물 목록', + 'ordering': ('-created_at',), + 'abstract': False, + }, + ), + ] diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index 94360319..b5e1fd16 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -9,25 +9,34 @@ class Meta(MetaDataModel.Meta): verbose_name = '소통 게시물' verbose_name_plural = '소통 게시물 목록' - article_id = models.OneToOneField( + article = models.OneToOneField( to='core.Article', on_delete=models.CASCADE, + related_name='communication_article', db_index=True, verbose_name='게시물', ) response_deadline = models.DateTimeField( - null=True, default=timezone.datetime.min.replace(tzinfo=timezone.utc), verbose_name='답변 요청 기한', ) - confirmed_by_school_at = models.BooleanField( + confirmed_by_school_at = models.DateTimeField( default=timezone.datetime.min.replace(tzinfo=timezone.utc), verbose_name='학교측 문의 확인 시각', ) - answered_at = models.BooleanField( + answered_at = models.DateTimeField( default=timezone.datetime.min.replace(tzinfo=timezone.utc), verbose_name='학교측 답변을 받은 시각', ) + + def get_status(self) -> int: + if self.response_deadline == timezone.datetime.min.replace(tzinfo=timezone.utc): + return 0 + if self.confirmed_by_school_at == timezone.datetime.min.replace(tzinfo=timezone.utc): + return 1 + if self.answered_at == timezone.datetime.min.replace(tzinfo=timezone.utc): + return 2 + return 3 diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 3fa2e92b..7b356112 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -3,9 +3,12 @@ from enum import Enum from django.utils.translation import gettext from rest_framework import serializers +from django.utils import timezone + from apps.core.documents import ArticleDocument from apps.core.models import Article, Board, Block, Scrap, ArticleHiddenReason from apps.core.serializers.board import BoardSerializer +from apps.core.serializers.communication_article import CommunicationArticleSerializer from apps.core.serializers.mixins.hidden import HiddenSerializerMixin, HiddenSerializerFieldMixin from apps.core.serializers.topic import TopicSerializer from apps.user.serializers.user import PublicUserSerializer @@ -312,7 +315,15 @@ def get_attachments(self, obj): # -> typing.Optional[list]: side_articles = serializers.SerializerMethodField( read_only=True, ) + communication_article_status = serializers.SerializerMethodField( + read_only=True, + ) + @staticmethod + def get_communication_article_status(obj) -> int: + if hasattr(obj, 'communication_article'): + return obj.communication_article.get_status() + return None class ArticleAttachmentType(Enum): NONE = 'NONE' @@ -342,6 +353,10 @@ class ArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSeriali read_only=True, ) + communication_article_status = serializers.SerializerMethodField( + read_only=True, + ) + def get_attachment_type(self, obj) -> str: if not self.visible_verdict(obj): return ArticleAttachmentType.NONE.value @@ -361,6 +376,12 @@ def get_attachment_type(self, obj) -> str: return ArticleAttachmentType.NON_IMAGE.value return ArticleAttachmentType.NONE.value + @staticmethod + def get_communication_article_status(obj) -> int: + if hasattr(obj, 'communication_article'): + return obj.communication_article.get_status() + return None + class BestArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): title = serializers.SerializerMethodField( From b17da91219b8513b48efd07532cfacc4bf940512 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Mon, 28 Mar 2022 21:04:16 +0900 Subject: [PATCH 016/101] Create communication article in article perform_create --- .../0039_board_is_school_communication.py | 18 ++++++++++++++++++ apps/core/models/board.py | 7 +++++++ apps/core/serializers/communication_article.py | 9 ++++----- apps/core/views/viewsets/article.py | 9 ++++++++- 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 apps/core/migrations/0039_board_is_school_communication.py diff --git a/apps/core/migrations/0039_board_is_school_communication.py b/apps/core/migrations/0039_board_is_school_communication.py new file mode 100644 index 00000000..ba592e26 --- /dev/null +++ b/apps/core/migrations/0039_board_is_school_communication.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.9 on 2022-03-28 11:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0038_communicationarticle'), + ] + + operations = [ + migrations.AddField( + model_name='board', + name='is_school_communication', + field=models.BooleanField(db_index=True, default=False, help_text='학교 소통 게시판 글임을 표시', verbose_name='학교와의 소통 게시판'), + ), + ] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 9e7efbee..681febbc 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -61,6 +61,13 @@ class Meta(MetaDataModel.Meta): db_index=True ) + is_school_communication = models.BooleanField( + verbose_name='학교와의 소통 게시판', + help_text='학교 소통 게시판 글임을 표시', + default=False, + db_index=True + ) + group_id = models.IntegerField( verbose_name='그룹 ID', default=1 diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index 979c565a..550b04bd 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -3,11 +3,10 @@ from apps.core.models.communication_article import CommunicationArticle -class BaseCommunicationArticleSerializer(MetaDataModelSerializer): +class CommunicationArticleSerializer(MetaDataModelSerializer): class Meta: model = CommunicationArticle - fields = '__all__' + fields = [] - -class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): - pass + # TODO: is_confirmed_by_school 등을 timestamp 말고 progress_statues enum으로 바꿔서 프론트 보내주기 + # 좋아요 30개 달설 전, 달성 후 확인 전, 확인 후 답변전, 답변 완료 diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index 2d2e1257..01c57d6e 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -17,7 +17,7 @@ Board, Comment, Vote, - Scrap, + Scrap, CommunicationArticle, ) from apps.core.filters.article import ArticleFilter from apps.core.permissions.article import ArticlePermission, ArticleKAISTPermission @@ -133,6 +133,13 @@ def perform_create(self, serializer): is_anonymous=Board.objects.get(pk=self.request.data['parent_board']).is_anonymous ) + instance = serializer.instance + if Board.objects.get(pk=self.request.data['parent_board']).is_school_communication: + # create communication article + CommunicationArticle.objects.create( + article=instance, + ) + def update(self, request, *args, **kwargs): article = self.get_object() if article.is_hidden_by_reported(): From 7017185a13197302fc831072b66e70b91fc552d3 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 28 Mar 2022 22:03:24 +0900 Subject: [PATCH 017/101] Update response_deadline when upvote >= threshold --- apps/core/models/article.py | 6 +++++- ara/settings/dev/__init__.py | 1 + ara/settings/prod/__init__.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 642c22d3..2adfd8db 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -15,7 +15,7 @@ from ara.classes.decorator import cache_by_user from ara.db.models import MetaDataModel from ara.sanitizer import sanitize -from ara.settings import HASH_SECRET_VALUE +from ara.settings import HASH_SECRET_VALUE, SCHOOL_RESPONSE_VOTE_THRESHOLD from .block import Block from .report import Report from .comment import Comment @@ -194,6 +194,10 @@ def update_vote_status(self): self.positive_vote_count = self.vote_set.filter(is_positive=True).count() + self.migrated_positive_vote_count self.negative_vote_count = self.vote_set.filter(is_positive=False).count() + self.migrated_negative_vote_count + if self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: + self.communication_article.response_deadline = timezone.now() + timezone.timedelta(days=14) + self.communication_article.save() + self.save() def is_hidden_by_reported(self) -> bool: diff --git a/ara/settings/dev/__init__.py b/ara/settings/dev/__init__.py index 42f58c7e..759e8a22 100644 --- a/ara/settings/dev/__init__.py +++ b/ara/settings/dev/__init__.py @@ -30,6 +30,7 @@ LOGGING['disable_existing_loggers'] = False REPORT_THRESHOLD = 4 +SCHOOL_RESPONSE_VOTE_THRESHOLD = 3 try: from .local_settings import * diff --git a/ara/settings/prod/__init__.py b/ara/settings/prod/__init__.py index 3cd16906..0028c42a 100644 --- a/ara/settings/prod/__init__.py +++ b/ara/settings/prod/__init__.py @@ -26,3 +26,4 @@ ) REPORT_THRESHOLD = 4 +SCHOOL_RESPONSE_VOTE_THRESHOLD = 30 From 88761aa54c66eda6a7efc75748ada21aa8718169 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Mon, 28 Mar 2022 22:52:25 +0900 Subject: [PATCH 018/101] Update communication article after receiving official response --- apps/core/models/signals/comment.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index 8be6354b..88a8e2c6 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -12,7 +12,6 @@ def notify_commented(comment): def update_article_comment_count(comment): parent_article = comment.get_parent_article() - parent_article.update_comment_count() def update_article_commented_at(comment): @@ -20,10 +19,17 @@ def update_article_commented_at(comment): article.commented_at = timezone.now() article.save() + def update_communication_article_status(comment): + article = comment.parent_article if comment.parent_article else comment.parent_comment.parent_article + if article.parent_board.is_school_communication and comment.created_by.profile.is_school_admin: + article.communication_article.answered_at = timezone.now() + article.communication_article.save() + if created: notify_commented(instance) update_article_comment_count(instance) update_article_commented_at(instance) + update_communication_article_status(instance) else: # in case of MetaDataModel's instance deleted From ec6132e339a06c9a0d2a1f8925d7aa1ffa223944 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Wed, 30 Mar 2022 01:09:19 +0900 Subject: [PATCH 019/101] Add realname comment --- apps/core/models/comment.py | 23 +++++++++++++++++++---- apps/core/serializers/comment.py | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index 50f600cf..4261061e 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -2,6 +2,7 @@ from enum import Enum from typing import Dict, Union import hashlib +import json from django.core.files.storage import default_storage from django.db import models, IntegrityError @@ -38,8 +39,8 @@ class Meta(MetaDataModel.Meta): verbose_name='본문', ) - is_anonymous = models.BooleanField( - default=False, + is_anonymous = models.SmallIntegerField( + default=0, verbose_name='익명', ) @@ -155,9 +156,9 @@ def update_report_count(self): # API 상에서 보이는 사용자 (익명일 경우 익명화된 글쓴이, 그 외는 그냥 글쓴이) @cached_property def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: - if not self.is_anonymous: + if self.is_anonymous == 0: return self.created_by - else: + elif self.is_anonymous == 1: parent_article = self.get_parent_article() parent_article_id = parent_article.id parent_article_created_by_id = parent_article.created_by.id @@ -184,6 +185,20 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'user': user_hash } } + else: + sso_info = self.created_by.profile.sso_user_info + user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] + user_profile_picture = make_random_profile_picture() + return { + 'id': 0, + 'username': user_realname, + 'profile': { + 'picture': default_storage.url(user_profile_picture), + 'nickname': user_realname, + 'user': user_realname + }, + } + @cache_by_user def hidden_reasons(self, user: settings.AUTH_USER_MODEL) -> typing.List: diff --git a/apps/core/serializers/comment.py b/apps/core/serializers/comment.py index 5409f4bb..f56639f0 100644 --- a/apps/core/serializers/comment.py +++ b/apps/core/serializers/comment.py @@ -33,7 +33,7 @@ def get_content(self, obj) -> typing.Optional[str]: return None def get_created_by(self, obj) -> dict: - if obj.is_anonymous: + if obj.is_anonymous == 1 or obj.is_anonymous == 2: return obj.postprocessed_created_by else: data = PublicUserSerializer(obj.postprocessed_created_by).data From 6d7390c0dd05654456c4fcfe3154eaa29dd6a15c Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 30 Mar 2022 08:15:10 +0900 Subject: [PATCH 020/101] Rename & Refactor make_random_profile_picture --- apps/user/views/viewsets/user.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/user/views/viewsets/user.py b/apps/user/views/viewsets/user.py index d72d1089..9fe99b5a 100644 --- a/apps/user/views/viewsets/user.py +++ b/apps/user/views/viewsets/user.py @@ -46,22 +46,19 @@ def _make_random_name() -> str: return temp_nickname -def make_random_profile_picture(hash_val=None) -> str: - colors = ['blue', 'red', 'gray'] - numbers = ['1', '2', '3'] - - if hash_val: - col = hash_val % len(colors) - num = (hash_val // 3) % len(numbers) - else: - col = random.randrange(len(colors)) - num = random.randrange(len(numbers)) - - temp_color = colors[col] - temp_num = numbers[num] - default_picture = f'user_profiles/default_pictures/{temp_color}-default{temp_num}.png' - - return default_picture +def get_profile_picture(hash_val=None) -> str: + colors = ['blue', 'red', 'gray'] + numbers = ['1', '2', '3'] + + if hash_val is None: + col = random.choice(colors) + num = random.choice(numbers) + else: + nidx, cidx = divmod(hash_val, len(colors)) + col = colors[cidx] + num = numbers[nidx % len(numbers)] + + return f'user_profiles/default_pictures/{col}-default{num}.png' class UserViewSet(ActionAPIViewSet): @@ -165,7 +162,7 @@ def sso_login_callback(self, request, *args, **kwargs): except UserProfile.DoesNotExist: # 회원가입 user_nickname = _make_random_name() - user_profile_picture = make_random_profile_picture() + user_profile_picture = get_profile_picture() email = user_info['email'] if email.split('@')[-1] == 'sso.sparcs.org': # sso에서 random하게 만든 이메일인 경우 From 95968659369aef45a5153cf36776dcb7f8f3db84 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 30 Mar 2022 08:17:57 +0900 Subject: [PATCH 021/101] Consider case when is_anonymous == 2 --- apps/core/models/article.py | 22 ++++++++++--------- apps/core/models/comment.py | 34 +++++++++++++++-------------- apps/core/views/viewsets/comment.py | 16 +++++++++----- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 578f6d5e..f644649f 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -12,7 +12,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext -from apps.user.views.viewsets import make_random_profile_picture, hashlib +from apps.user.views.viewsets import get_profile_picture, hashlib from ara.classes.decorator import cache_by_user from ara.db.models import MetaDataModel from ara.sanitizer import sanitize @@ -209,13 +209,14 @@ def created_by_nickname(self): def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: if self.is_anonymous == 0: return self.created_by - elif self.is_anonymous == 1: - user_unique_num = self.created_by.id + self.id + HASH_SECRET_VALUE - user_unique_encoding = str(hex(user_unique_num)).encode('utf-8') - user_hash = hashlib.sha224(user_unique_encoding).hexdigest() - user_hash_int = int(user_hash[-4:], 16) - user_profile_picture = make_random_profile_picture(user_hash_int) - + + user_unique_num = self.created_by.id + self.id + HASH_SECRET_VALUE + user_unique_encoding = str(hex(user_unique_num)).encode('utf-8') + user_hash = hashlib.sha224(user_unique_encoding).hexdigest() + user_hash_int = int(user_hash[-4:], 16) + user_profile_picture = get_profile_picture(user_hash_int) + + if self.is_anonymous == 1: return { 'id': user_hash, 'username': gettext('anonymous'), @@ -225,10 +226,11 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'user': gettext('anonymous') }, } - else: + + if self.is_anonymous == 2: sso_info = self.created_by.profile.sso_user_info user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] - user_profile_picture = make_random_profile_picture() + return { 'id': 0, 'username': user_realname, diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index 4261061e..6a08b806 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -12,7 +12,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext -from apps.user.views.viewsets import NOUNS, make_random_profile_picture +from apps.user.views.viewsets import NOUNS, get_profile_picture from ara.classes.decorator import cache_by_user from ara.db.models import MetaDataModel, MetaDataQuerySet from ara.sanitizer import sanitize @@ -158,19 +158,20 @@ def update_report_count(self): def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: if self.is_anonymous == 0: return self.created_by - elif self.is_anonymous == 1: - parent_article = self.get_parent_article() - parent_article_id = parent_article.id - parent_article_created_by_id = parent_article.created_by.id - comment_created_by_id = self.created_by.id - - # 댓글 작성자는 (작성자 id + parent article id + 시크릿)을 해시한 값으로 구별합니다. - user_unique_num = comment_created_by_id + parent_article_id + HASH_SECRET_VALUE - user_unique_encoding = str(hex(user_unique_num)).encode('utf-8') - user_hash = hashlib.sha224(user_unique_encoding).hexdigest() - user_hash_int = int(user_hash[-4:], 16) - user_profile_picture = make_random_profile_picture(user_hash_int) - + + parent_article = self.get_parent_article() + parent_article_id = parent_article.id + parent_article_created_by_id = parent_article.created_by.id + comment_created_by_id = self.created_by.id + + # 댓글 작성자는 (작성자 id + parent article id + 시크릿)을 해시한 값으로 구별합니다. + user_unique_num = comment_created_by_id + parent_article_id + HASH_SECRET_VALUE + user_unique_encoding = str(hex(user_unique_num)).encode('utf-8') + user_hash = hashlib.sha224(user_unique_encoding).hexdigest() + user_hash_int = int(user_hash[-4:], 16) + user_profile_picture = get_profile_picture(user_hash_int) + + if self.is_anonymous == 1: if parent_article_created_by_id == comment_created_by_id: user_name = gettext('author') else: @@ -185,10 +186,11 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'user': user_hash } } - else: + + if self.is_anonymous == 2: sso_info = self.created_by.profile.sso_user_info user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] - user_profile_picture = make_random_profile_picture() + return { 'id': 0, 'username': user_realname, diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 340cea6d..7df16370 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -58,16 +58,22 @@ def create(self, request, *args, **kwargs): def perform_create(self, serializer): parent_article_id = self.request.data.get('parent_article') - parent_article = parent_article_id and Article.objects.filter(id=parent_article_id).first() - parent_article_is_anonymous = (parent_article and parent_article.is_anonymous) or False + parent_article = parent_article_id and Article.objects.get(id=parent_article_id) + parent_article_is_anonymous = (parent_article and parent_article.is_anonymous) or 0 parent_comment_id = self.request.data.get('parent_comment') - parent_comment = parent_comment_id and Comment.objects.filter(id=parent_comment_id).first() - parent_comment_is_anonymous = (parent_comment and parent_comment.is_anonymous) or False + parent_comment = parent_comment_id and Comment.objects.get(id=parent_comment_id) + parent_comment_is_anonymous = (parent_comment and parent_comment.is_anonymous) or 0 + + anonymous_status = 0 + for stat in (1, 2): + if parent_article_is_anonymous == stat or parent_comment_is_anonymous == stat: + anonymous_status = stat + break serializer.save( created_by=self.request.user, - is_anonymous=parent_article_is_anonymous or parent_comment_is_anonymous, + is_anonymous=anonymous_status, ) def retrieve(self, request, *args, **kwargs): From 4ea8db3ed701ad07c4d17367798726e00eedfb0d Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 31 Mar 2022 19:29:48 +0900 Subject: [PATCH 022/101] Create communication article admin page --- apps/core/admin.py | 20 ++++++++++++++++++++ apps/core/models/communication_article.py | 17 ++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/core/admin.py b/apps/core/admin.py index ec6fcdfe..e33d6f85 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -17,6 +17,7 @@ BestSearch, Report, Comment, + CommunicationArticle ) @@ -224,3 +225,22 @@ class ReportAdmin(MetaDataModelAdmin): 'type', 'content', ) + + +@admin.register(CommunicationArticle) +class CommunicationArticleAdmin(MetaDataModelAdmin): + list_filter = ( + 'response_deadline', + 'confirmed_by_school_at', + 'answered_at', + ) + list_display = ( + 'article', + 'get_status_string', + 'response_deadline', + 'confirmed_by_school_at', + 'answered_at', + ) + raw_id_fields = ( + 'article', + ) diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index b5e1fd16..1cb2bd26 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -1,5 +1,6 @@ from django.db import models from django.utils import timezone +from django.contrib import admin from ara.db.models import MetaDataModel @@ -33,10 +34,20 @@ class Meta(MetaDataModel.Meta): ) def get_status(self) -> int: - if self.response_deadline == timezone.datetime.min.replace(tzinfo=timezone.utc): + min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) + if self.response_deadline == min_time: return 0 - if self.confirmed_by_school_at == timezone.datetime.min.replace(tzinfo=timezone.utc): + if self.confirmed_by_school_at == min_time: return 1 - if self.answered_at == timezone.datetime.min.replace(tzinfo=timezone.utc): + if self.answered_at == min_time: return 2 return 3 + + @admin.display(description='진행 상황') + def get_status_string(self) -> str: + status_list = ['소통 중', '답변 대기 중', '답변 준비 중', '답변 완료'] + status = self.get_status() + return status_list[status] + + def __str__(self): + return self.article.title From 7bc499e8a57e228ec6f500d20bba28b21bab085d Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 31 Mar 2022 19:30:32 +0900 Subject: [PATCH 023/101] Add test for communication article --- tests/test_communication_article.py | 85 +++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/test_communication_article.py diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py new file mode 100644 index 00000000..f5e20b37 --- /dev/null +++ b/tests/test_communication_article.py @@ -0,0 +1,85 @@ +import pytest +from django.utils import timezone +from apps.core.models import Article, Board, CommunicationArticle +from tests.conftest import RequestSetting, TestCase + + +@pytest.fixture(scope='class') +def set_board(request): + request.cls.board = Board.objects.create( + slug='with-school', + ko_name='학교와의 게시판 (테스트)', + en_name='With School (Test)', + ko_description='학교와의 게시판 (테스트)', + en_description='With School (Test)', + is_school_communication=True + ) + + +@pytest.fixture(scope='class') +def set_article(request): + # After defining set_board + request.cls.article = Article.objects.create( + title='Article Title', + content='Article Content', + content_text='Article Content Text', + is_anonymous=False, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + comment_count=0, + report_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=request.cls.user, + parent_board=request.cls.board + ) + + +@pytest.fixture(scope='class') +def set_communication_article(request): + # After defining set_article + request.cls.communication_article = CommunicationArticle.objects.create( + article=request.cls.article + ) + + +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', + 'set_user_client4', 'set_board', 'set_article', 'set_communication_article') +class TestCommunicationArticle(TestCase, RequestSetting): + # 소통 게시물이 communication_article 필드 가지는지 확인 + def test_article_has_communication_article(self): + assert self.article.parent_board.is_school_communication is True + assert hasattr(self.article, 'communication_article') + + # 필드 디폴트 값 확인 + def test_default_fields(self): + min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) + assert all([ + self.communication_article.response_deadline == min_time, + self.communication_article.confirmed_by_school_at == min_time, + self.communication_article.answered_at == min_time + ]) + + # get_status 메소드 동작 확인 + def test_get_status(self): + # vote 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD보다 작으면 status == 0 + res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data + assert res.get('communication_article_status') == 0 + + # 게시물에 좋아요 3개 추가 (작성일 기준 SCHOOL_RESPONSE_VOTE_THRESHOLD == 3) + users_tuple = (self.user2, self.user3, self.user4) + for user in users_tuple: + self.http_request(user, 'post', f'articles/{self.article.id}/vote_positive') + + # 좋아요 개수 업데이트 확인 + res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data + assert res.get('positive_vote_count') == len(users_tuple) + + # 좋아요 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD 이상이므로 status == 1 + assert res.get('communication_article_status') == 1 + + # response_deadline 업데이트 확인 + min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) + self.communication_article.refresh_from_db() + assert self.communication_article.response_deadline != min_time From bcd4e14357f460087f6e7dc7debea5ddd3054a02 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Thu, 31 Mar 2022 21:39:06 +0900 Subject: [PATCH 024/101] Add user unique id in realname board, Fix profile picture in board list --- apps/core/models/article.py | 5 +++-- apps/core/models/comment.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index f644649f..05e83e55 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -215,6 +215,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: user_hash = hashlib.sha224(user_unique_encoding).hexdigest() user_hash_int = int(user_hash[-4:], 16) user_profile_picture = get_profile_picture(user_hash_int) + user_profile_picture_realname = get_profile_picture(self.created_by.id + HASH_SECRET_VALUE) if self.is_anonymous == 1: return { @@ -232,10 +233,10 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] return { - 'id': 0, + 'id': user_unique_num, 'username': user_realname, 'profile': { - 'picture': default_storage.url(user_profile_picture), + 'picture': default_storage.url(user_profile_picture_realname), 'nickname': user_realname, 'user': user_realname }, diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index 6a08b806..62222ea5 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -192,7 +192,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] return { - 'id': 0, + 'id': user_unique_num, 'username': user_realname, 'profile': { 'picture': default_storage.url(user_profile_picture), From 73decb22db9c4aebb0ab912b669f5e4611928600 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Sat, 2 Apr 2022 00:57:33 +0900 Subject: [PATCH 025/101] Add response status field to CommunicationArticle --- ...municationarticle_school_response_status.py | 18 ++++++++++++++++++ apps/core/models/communication_article.py | 16 +++++----------- apps/core/serializers/article.py | 4 ++-- 3 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 apps/core/migrations/0040_communicationarticle_school_response_status.py diff --git a/apps/core/migrations/0040_communicationarticle_school_response_status.py b/apps/core/migrations/0040_communicationarticle_school_response_status.py new file mode 100644 index 00000000..31fe2c79 --- /dev/null +++ b/apps/core/migrations/0040_communicationarticle_school_response_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.9 on 2022-03-31 18:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0039_board_is_school_communication'), + ] + + operations = [ + migrations.AddField( + model_name='communicationarticle', + name='school_response_status', + field=models.SmallIntegerField(default=0, verbose_name='답변 진행 상황'), + ), + ] diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index 1cb2bd26..2f59eeeb 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -33,21 +33,15 @@ class Meta(MetaDataModel.Meta): verbose_name='학교측 답변을 받은 시각', ) - def get_status(self) -> int: - min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) - if self.response_deadline == min_time: - return 0 - if self.confirmed_by_school_at == min_time: - return 1 - if self.answered_at == min_time: - return 2 - return 3 + school_response_status = models.SmallIntegerField( + default=0, + verbose_name='답변 진행 상황', + ) @admin.display(description='진행 상황') def get_status_string(self) -> str: status_list = ['소통 중', '답변 대기 중', '답변 준비 중', '답변 완료'] - status = self.get_status() - return status_list[status] + return status_list[self.school_response_status] def __str__(self): return self.article.title diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 7b356112..19b0dd90 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -322,7 +322,7 @@ def get_attachments(self, obj): # -> typing.Optional[list]: @staticmethod def get_communication_article_status(obj) -> int: if hasattr(obj, 'communication_article'): - return obj.communication_article.get_status() + return obj.communication_article.school_response_status return None class ArticleAttachmentType(Enum): @@ -379,7 +379,7 @@ def get_attachment_type(self, obj) -> str: @staticmethod def get_communication_article_status(obj) -> int: if hasattr(obj, 'communication_article'): - return obj.communication_article.get_status() + return obj.communication_article.school_response_status return None From 73b97a12a48d2677f4860ed2c8e6453ce3a0549b Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Sat, 2 Apr 2022 01:04:30 +0900 Subject: [PATCH 026/101] Add CommunicationArticleViewSet with filter --- apps/core/views/router.py | 10 +++++++--- apps/core/views/viewsets/__init__.py | 1 + .../views/viewsets/communication_article.py | 20 +++++++++++++++++++ ara/settings/django.py | 1 + 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 apps/core/views/viewsets/communication_article.py diff --git a/apps/core/views/router.py b/apps/core/views/router.py index 8afe7d11..13f27c65 100644 --- a/apps/core/views/router.py +++ b/apps/core/views/router.py @@ -52,16 +52,20 @@ viewset=viewsets.NotificationViewSet, ) - # FAQViewSet router.register( prefix=r'faqs', viewset=viewsets.FAQViewSet, ) - # BestSearchViewSet router.register( - prefix=f'best_searches', + prefix=r'best_searches', viewset=viewsets.BestSearchViewSet, ) + +# CommunicationArticleViewSet +router.register( + prefix=r'communication_articles', + viewset=viewsets.CommunicationArticleViewSet, +) diff --git a/apps/core/views/viewsets/__init__.py b/apps/core/views/viewsets/__init__.py index 89e9160d..a280de75 100644 --- a/apps/core/views/viewsets/__init__.py +++ b/apps/core/views/viewsets/__init__.py @@ -8,3 +8,4 @@ from .scrap import * from .faq import * from .best_search import * +from .communication_article import * diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py new file mode 100644 index 00000000..017c1ef2 --- /dev/null +++ b/apps/core/views/viewsets/communication_article.py @@ -0,0 +1,20 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, viewsets + +from ara.classes.viewset import ActionAPIViewSet + +from apps.core.models.communication_article import CommunicationArticle +from apps.core.serializers.communication_article import CommunicationArticleSerializer + + +class CommunicationArticleViewSet(viewsets.ReadOnlyModelViewSet, ActionAPIViewSet): + queryset = CommunicationArticle.objects.all() + serializer_class = CommunicationArticleSerializer + filter_backends = [filters.OrderingFilter, DjangoFilterBackend] + + # usage: /api/communication_articles/?ordering=created_at + ordering_fields = ['created_at', 'article__positive_vote_count'] + ordering = ['-article__positive_vote_count'] # default: 추천수 내림차순 + + # usage: /api/communication_articles/?school_response_status=1 + filterset_fields = ['school_response_status'] diff --git a/ara/settings/django.py b/ara/settings/django.py index 35a99082..f9dc6dcc 100644 --- a/ara/settings/django.py +++ b/ara/settings/django.py @@ -31,6 +31,7 @@ 'drf_yasg', 'cacheops', 'django_elasticsearch_dsl', + 'django_filters', 'apps.core', 'apps.user', From 43575a38adb8c53e74cdcd631fc997cd742870fc Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Sat, 2 Apr 2022 01:43:58 +0900 Subject: [PATCH 027/101] Update school_response_status when needed --- apps/core/models/article.py | 4 +++- apps/core/models/signals/comment.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 2adfd8db..48ced66e 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -194,8 +194,10 @@ def update_vote_status(self): self.positive_vote_count = self.vote_set.filter(is_positive=True).count() + self.migrated_positive_vote_count self.negative_vote_count = self.vote_set.filter(is_positive=False).count() + self.migrated_negative_vote_count - if self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: + if self.parent_board.is_school_communication and self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: self.communication_article.response_deadline = timezone.now() + timezone.timedelta(days=14) + if self.communication_article.school_response_status < 1: + self.communication_article.school_response_status = 1 self.communication_article.save() self.save() diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index 88a8e2c6..80871504 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -23,6 +23,7 @@ def update_communication_article_status(comment): article = comment.parent_article if comment.parent_article else comment.parent_comment.parent_article if article.parent_board.is_school_communication and comment.created_by.profile.is_school_admin: article.communication_article.answered_at = timezone.now() + article.communication_article.school_response_status = 3 article.communication_article.save() if created: From fd65814fd7acaa922508b95abff87b5a15c936b8 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Sat, 2 Apr 2022 03:59:32 +0900 Subject: [PATCH 028/101] Test if communication article status updates to 3 --- tests/test_communication_article.py | 47 ++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index f5e20b37..391b02fa 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -1,7 +1,28 @@ import pytest from django.utils import timezone -from apps.core.models import Article, Board, CommunicationArticle +from django.contrib.auth.models import User + +from apps.core.models import Article, Board, Comment, CommunicationArticle +from apps.user.models import UserProfile + from tests.conftest import RequestSetting, TestCase +from rest_framework.test import APIClient + + +@pytest.fixture(scope='class') +def set_school_admin(request): + request.cls.school_admin, _ = User.objects.get_or_create( + username='School Admin', + email='schooladmin@sparcs.org' + ) + if not hasattr(request.cls.school_admin, 'profile'): + UserProfile.objects.get_or_create( + user=request.cls.school_admin, + nickname='School Admin', + agree_terms_of_service_at=timezone.now(), + is_school_admin=True + ) + request.cls.api_client = APIClient() @pytest.fixture(scope='class') @@ -44,8 +65,8 @@ def set_communication_article(request): ) -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', - 'set_user_client4', 'set_board', 'set_article', 'set_communication_article') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', + 'set_school_admin', 'set_board', 'set_article', 'set_communication_article') class TestCommunicationArticle(TestCase, RequestSetting): # 소통 게시물이 communication_article 필드 가지는지 확인 def test_article_has_communication_article(self): @@ -61,8 +82,8 @@ def test_default_fields(self): self.communication_article.answered_at == min_time ]) - # get_status 메소드 동작 확인 - def test_get_status(self): + # school_response_status 업데이트 확인 + def test_school_response_status(self): # vote 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD보다 작으면 status == 0 res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data assert res.get('communication_article_status') == 0 @@ -83,3 +104,19 @@ def test_get_status(self): min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) self.communication_article.refresh_from_db() assert self.communication_article.response_deadline != min_time + + # is_school_admin = True인 사용자가 코멘트 작성 + Comment.objects.create( + content = 'School Official Comment', + is_anonymous = False, + created_by = self.school_admin, + parent_article = self.article + ) + + # school admin이 코멘트 작성했으므로 status == 3 + res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data + assert res.get('communication_article_status') == 3 + + # answered_at 업데이트 확인 + self.communication_article.refresh_from_db() + assert self.communication_article.answered_at != min_time From 5bbab4b0bf8b00b5c07accc3d2b98ed8f0d3faa4 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 4 Apr 2022 20:12:55 +0900 Subject: [PATCH 029/101] Change school response status to Enum --- apps/core/models/article.py | 5 +++-- apps/core/models/communication_article.py | 11 ++++++++++- apps/core/models/signals/comment.py | 3 ++- apps/core/serializers/article.py | 4 ++-- tests/test_communication_article.py | 15 ++++++++------- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 48ced66e..84e22f20 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -19,6 +19,7 @@ from .block import Block from .report import Report from .comment import Comment +from .communication_article import SchoolResponseStatus class ArticleHiddenReason(str, Enum): @@ -196,8 +197,8 @@ def update_vote_status(self): if self.parent_board.is_school_communication and self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: self.communication_article.response_deadline = timezone.now() + timezone.timedelta(days=14) - if self.communication_article.school_response_status < 1: - self.communication_article.school_response_status = 1 + if self.communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: + self.communication_article.school_response_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM self.communication_article.save() self.save() diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index 2f59eeeb..0fc29317 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -4,6 +4,15 @@ from ara.db.models import MetaDataModel +from enum import IntEnum + + +class SchoolResponseStatus(IntEnum): + BEFORE_UPVOTE_THRESHOLD = 0 + BEFORE_SCHOOL_CONFIRM = 1 + PREPARING_ANSWER = 2 + ANSWER_DONE = 3 + class CommunicationArticle(MetaDataModel): class Meta(MetaDataModel.Meta): @@ -34,7 +43,7 @@ class Meta(MetaDataModel.Meta): ) school_response_status = models.SmallIntegerField( - default=0, + default=SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD, verbose_name='답변 진행 상황', ) diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index 80871504..c4b90752 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -3,6 +3,7 @@ from django.utils import timezone from apps.core.models import Comment, Notification +from apps.core.models.communication_article import SchoolResponseStatus @receiver(models.signals.post_save, sender=Comment) @@ -23,7 +24,7 @@ def update_communication_article_status(comment): article = comment.parent_article if comment.parent_article else comment.parent_comment.parent_article if article.parent_board.is_school_communication and comment.created_by.profile.is_school_admin: article.communication_article.answered_at = timezone.now() - article.communication_article.school_response_status = 3 + article.communication_article.school_response_status = SchoolResponseStatus.ANSWER_DONE article.communication_article.save() if created: diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 19b0dd90..7c98acb8 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -320,7 +320,7 @@ def get_attachments(self, obj): # -> typing.Optional[list]: ) @staticmethod - def get_communication_article_status(obj) -> int: + def get_communication_article_status(obj): if hasattr(obj, 'communication_article'): return obj.communication_article.school_response_status return None @@ -377,7 +377,7 @@ def get_attachment_type(self, obj) -> str: return ArticleAttachmentType.NONE.value @staticmethod - def get_communication_article_status(obj) -> int: + def get_communication_article_status(obj): if hasattr(obj, 'communication_article'): return obj.communication_article.school_response_status return None diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 391b02fa..c82122e5 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -2,7 +2,8 @@ from django.utils import timezone from django.contrib.auth.models import User -from apps.core.models import Article, Board, Comment, CommunicationArticle +from apps.core.models import Article, Board, Comment +from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus from apps.user.models import UserProfile from tests.conftest import RequestSetting, TestCase @@ -84,9 +85,9 @@ def test_default_fields(self): # school_response_status 업데이트 확인 def test_school_response_status(self): - # vote 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD보다 작으면 status == 0 + # vote 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD보다 작으면 status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data - assert res.get('communication_article_status') == 0 + assert res.get('communication_article_status') == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD # 게시물에 좋아요 3개 추가 (작성일 기준 SCHOOL_RESPONSE_VOTE_THRESHOLD == 3) users_tuple = (self.user2, self.user3, self.user4) @@ -97,8 +98,8 @@ def test_school_response_status(self): res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data assert res.get('positive_vote_count') == len(users_tuple) - # 좋아요 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD 이상이므로 status == 1 - assert res.get('communication_article_status') == 1 + # 좋아요 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD 이상이므로 status == SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + assert res.get('communication_article_status') == SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM # response_deadline 업데이트 확인 min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) @@ -113,9 +114,9 @@ def test_school_response_status(self): parent_article = self.article ) - # school admin이 코멘트 작성했으므로 status == 3 + # school admin이 코멘트 작성했으므로 status == SchoolResponseStatus.ANSWER_DONE res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data - assert res.get('communication_article_status') == 3 + assert res.get('communication_article_status') == SchoolResponseStatus.ANSWER_DONE # answered_at 업데이트 확인 self.communication_article.refresh_from_db() From 3d3240a213e973dad5aa94d9e5e71378fc2ef6fb Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 4 Apr 2022 20:51:21 +0900 Subject: [PATCH 030/101] Fix notifications TypeError Change is_read's `name` to `field_name` https://django-filter.readthedocs.io/en/stable/ref/filters.html --- apps/core/filters/notification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/core/filters/notification.py b/apps/core/filters/notification.py index 2cd1806c..37ecdf25 100644 --- a/apps/core/filters/notification.py +++ b/apps/core/filters/notification.py @@ -29,13 +29,13 @@ class Meta: } is_read = filters.BooleanFilter( - name='is_read', + field_name='is_read', label='조회 여부', method='get_is_read', ) @staticmethod - def get_is_read(queryset, name, value): + def get_is_read(queryset, field_name, value): return queryset.filter( notification_read_log_set__is_read=value, ) From e52e3414cb343dd8ef400968ad1540568998ced9 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Mon, 4 Apr 2022 23:09:45 +0900 Subject: [PATCH 031/101] Add API for school admin's confirmation of communication article --- .../0041_communication_article_primary_key.py | 23 ++++++++++++++ apps/core/models/communication_article.py | 1 + .../core/serializers/communication_article.py | 15 +++++++++- .../views/viewsets/communication_article.py | 30 +++++++++++++++++-- 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 apps/core/migrations/0041_communication_article_primary_key.py diff --git a/apps/core/migrations/0041_communication_article_primary_key.py b/apps/core/migrations/0041_communication_article_primary_key.py new file mode 100644 index 00000000..a4f8230c --- /dev/null +++ b/apps/core/migrations/0041_communication_article_primary_key.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.9 on 2022-04-04 12:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0040_communicationarticle_school_response_status'), + ] + + operations = [ + migrations.RemoveField( + model_name='communicationarticle', + name='id', + ), + migrations.AlterField( + model_name='communicationarticle', + name='article', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='communication_article', serialize=False, to='core.article', verbose_name='게시물'), + ), + ] diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index 0fc29317..d9abb1d4 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -25,6 +25,7 @@ class Meta(MetaDataModel.Meta): related_name='communication_article', db_index=True, verbose_name='게시물', + primary_key=True, ) response_deadline = models.DateTimeField( diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index 550b04bd..5d5f9abe 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -6,7 +6,20 @@ class CommunicationArticleSerializer(MetaDataModelSerializer): class Meta: model = CommunicationArticle - fields = [] + fields = '__all__' # TODO: is_confirmed_by_school 등을 timestamp 말고 progress_statues enum으로 바꿔서 프론트 보내주기 # 좋아요 30개 달설 전, 달성 후 확인 전, 확인 후 답변전, 답변 완료 + +# class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): +# class Meta(BaseCommunicationArticleSerializer.Meta): + + +class CommunicationArticleUpdateActionSerializer(CommunicationArticleSerializer): + class Meta(CommunicationArticleSerializer.Meta): + read_only_fields = ( + 'article', + 'response_deadline', + 'answered_at', + 'school_response_status' + ) diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index 017c1ef2..70f6b83a 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -1,15 +1,23 @@ +from django.utils.translation import gettext +from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, viewsets +from rest_framework import status, filters, viewsets, response, permissions from ara.classes.viewset import ActionAPIViewSet from apps.core.models.communication_article import CommunicationArticle -from apps.core.serializers.communication_article import CommunicationArticleSerializer +from apps.core.serializers.communication_article import CommunicationArticleSerializer, CommunicationArticleUpdateActionSerializer -class CommunicationArticleViewSet(viewsets.ReadOnlyModelViewSet, ActionAPIViewSet): +class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): queryset = CommunicationArticle.objects.all() serializer_class = CommunicationArticleSerializer + action_serializer_class = { + 'update': CommunicationArticleUpdateActionSerializer, + } + permission_classes = ( + permissions.IsAuthenticated, + ) filter_backends = [filters.OrderingFilter, DjangoFilterBackend] # usage: /api/communication_articles/?ordering=created_at @@ -18,3 +26,19 @@ class CommunicationArticleViewSet(viewsets.ReadOnlyModelViewSet, ActionAPIViewSe # usage: /api/communication_articles/?school_response_status=1 filterset_fields = ['school_response_status'] + + # 학교 담당자가 신문고 게시글에 대해 `확인했습니다` 버튼을 누른 경우 + def update(self, request, *args, **kwargs): + # user가 학교 담당자인지 확인 + if not self.request.user.profile.is_school_admin: + return response.Response({'message': gettext('You are not authorized to access this feature')}, + status=status.HTTP_403_FORBIDDEN) + elif self.get_object().confirmed_by_school_at != timezone.datetime.min.replace(tzinfo=timezone.utc): + return response.Response(status=status.HTTP_200_OK) + return super().update(request, *args, **kwargs) + + def perform_update(self, serializer): + serializer.save( + confirmed_by_school_at=timezone.now(), + ) + return super().perform_update(serializer) From 92f399eeabe4f0b285246c7aaae16ed45d9c884a Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Mon, 4 Apr 2022 23:33:12 +0900 Subject: [PATCH 032/101] Rename is_anonymous to name_type, Change type boolean to smallint --- apps/core/admin.py | 8 ++-- apps/core/filters/article.py | 2 +- apps/core/filters/comment.py | 2 +- .../0038_rename_is_anonymous_to_name_type.py | 44 +++++++++++++++++++ apps/core/models/article.py | 13 +++--- apps/core/models/board.py | 14 +++--- apps/core/models/comment.py | 13 +++--- apps/core/serializers/article.py | 5 ++- apps/core/serializers/comment.py | 5 ++- apps/core/views/viewsets/article.py | 9 ++-- apps/core/views/viewsets/comment.py | 9 ++-- ara/locale/ko/LC_MESSAGES/django.po | 3 +- tests/test_article_search.py | 3 +- tests/test_articles.py | 37 ++++++++-------- tests/test_block.py | 15 ++++--- tests/test_comments.py | 39 ++++++++-------- tests/test_recent.py | 3 +- tests/test_report.py | 5 ++- tests/test_user.py | 7 +-- 19 files changed, 149 insertions(+), 87 deletions(-) create mode 100644 apps/core/migrations/0038_rename_is_anonymous_to_name_type.py diff --git a/apps/core/admin.py b/apps/core/admin.py index ec6fcdfe..58afe975 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -86,7 +86,7 @@ class FAQAdmin(MetaDataModelAdmin): @admin.register(Article) class ArticleAdmin(MetaDataModelAdmin): list_filter = ( - 'is_anonymous', + 'name_type', 'is_content_sexual', 'is_content_social', 'parent_topic', @@ -98,7 +98,7 @@ class ArticleAdmin(MetaDataModelAdmin): 'hit_count', 'positive_vote_count', 'negative_vote_count', - 'is_anonymous', + 'name_type', 'is_content_sexual', 'is_content_social', 'report_count', @@ -128,14 +128,14 @@ def restore_hidden_articles(self, request, queryset): @admin.register(Comment) class CommentAdmin(MetaDataModelAdmin): list_filter = ( - 'is_anonymous', + 'name_type', HiddenContentListFilter, ) list_display = ( 'content', 'positive_vote_count', 'negative_vote_count', - 'is_anonymous', + 'name_type', 'report_count', 'hidden_at', ) diff --git a/apps/core/filters/article.py b/apps/core/filters/article.py index 4faecc33..c8b9123a 100644 --- a/apps/core/filters/article.py +++ b/apps/core/filters/article.py @@ -19,7 +19,7 @@ class Meta: 'content_text': [ 'contains', ], - 'is_anonymous': [ + 'name_type': [ 'exact', ], 'is_content_sexual': [ diff --git a/apps/core/filters/comment.py b/apps/core/filters/comment.py index dfbeb438..69c824dc 100644 --- a/apps/core/filters/comment.py +++ b/apps/core/filters/comment.py @@ -16,7 +16,7 @@ class Meta: 'parent_comment': [ 'exact', ], - 'is_anonymous': [ + 'name_type': [ 'exact', ], 'created_by': [ diff --git a/apps/core/migrations/0038_rename_is_anonymous_to_name_type.py b/apps/core/migrations/0038_rename_is_anonymous_to_name_type.py new file mode 100644 index 00000000..96381655 --- /dev/null +++ b/apps/core/migrations/0038_rename_is_anonymous_to_name_type.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.9 on 2022-04-04 13:49 + +import apps.core.models.board +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0037_board_banner_url'), + ] + + operations = [ + migrations.RenameField( + model_name='article', + old_name='is_anonymous', + new_name='name_type', + ), + migrations.AlterField( + model_name='article', + name='name_type', + field=models.SmallIntegerField(default=apps.core.models.board.BoardNameType['REGULAR'], verbose_name='익명 혹은 실명 여부'), + ), + migrations.RenameField( + model_name='board', + old_name='is_anonymous', + new_name='name_type', + ), + migrations.AlterField( + model_name='board', + name='name_type', + field=models.SmallIntegerField(db_index=True, default=apps.core.models.board.BoardNameType['REGULAR'], help_text='게시판의 글과 댓글들이 익명 혹은 실명이도록 합니다.', verbose_name='익명/실명 게시판'), + ), + migrations.RenameField( + model_name='comment', + old_name='is_anonymous', + new_name='name_type', + ), + migrations.AlterField( + model_name='comment', + name='name_type', + field=models.SmallIntegerField(default=apps.core.models.board.BoardNameType['REGULAR'], verbose_name='익명 혹은 실명'), + ), + ] diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 05e83e55..7fa8ed0a 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -20,6 +20,7 @@ from .block import Block from .report import Report from .comment import Comment +from .board import BoardNameType class ArticleHiddenReason(str, Enum): @@ -47,9 +48,9 @@ class Meta(MetaDataModel.Meta): verbose_name='text 형식 본문', ) - is_anonymous = models.SmallIntegerField( - default=False, - verbose_name='익명', + name_type = models.SmallIntegerField( + default=BoardNameType.REGULAR, + verbose_name='익명 혹은 실명 여부', ) is_content_sexual = models.BooleanField( default=False, @@ -207,7 +208,7 @@ def created_by_nickname(self): # API 상에서 보이는 사용자 (익명일 경우 익명화된 글쓴이, 그 외는 그냥 글쓴이) @cached_property def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: - if self.is_anonymous == 0: + if self.name_type == BoardNameType.REGULAR: return self.created_by user_unique_num = self.created_by.id + self.id + HASH_SECRET_VALUE @@ -217,7 +218,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: user_profile_picture = get_profile_picture(user_hash_int) user_profile_picture_realname = get_profile_picture(self.created_by.id + HASH_SECRET_VALUE) - if self.is_anonymous == 1: + if self.name_type == BoardNameType.ANONYMOUS: return { 'id': user_hash, 'username': gettext('anonymous'), @@ -228,7 +229,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: }, } - if self.is_anonymous == 2: + if self.name_type == BoardNameType.REALNAME: sso_info = self.created_by.profile.sso_user_info user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 21109481..fe4883f9 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -1,9 +1,13 @@ from django.db import models - +from enum import IntEnum from django_extensions.db.fields import AutoSlugField from ara.db.models import MetaDataModel +class BoardNameType(IntEnum): + REGULAR = 0 + ANONYMOUS = 1 + REALNAME = 2 class Board(MetaDataModel): class Meta(MetaDataModel.Meta): @@ -54,10 +58,10 @@ class Meta(MetaDataModel.Meta): db_index=True, ) - is_anonymous = models.SmallIntegerField( - verbose_name='익명 게시판', - help_text='게시판의 글과 댓글들이 익명이도록 합니다.', - default=0, + name_type = models.SmallIntegerField( + verbose_name='익명/실명 게시판', + help_text='게시판의 글과 댓글들이 익명 혹은 실명이도록 합니다.', + default=BoardNameType.REGULAR, db_index=True ) diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index 62222ea5..decacf02 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -19,6 +19,7 @@ from ara.settings import HASH_SECRET_VALUE from .block import Block from .report import Report +from .board import BoardNameType class CommentHiddenReason(Enum): @@ -39,9 +40,9 @@ class Meta(MetaDataModel.Meta): verbose_name='본문', ) - is_anonymous = models.SmallIntegerField( - default=0, - verbose_name='익명', + name_type = models.SmallIntegerField( + default=BoardNameType.REGULAR, + verbose_name='익명 혹은 실명', ) report_count = models.IntegerField( @@ -156,7 +157,7 @@ def update_report_count(self): # API 상에서 보이는 사용자 (익명일 경우 익명화된 글쓴이, 그 외는 그냥 글쓴이) @cached_property def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: - if self.is_anonymous == 0: + if self.name_type == BoardNameType.REGULAR: return self.created_by parent_article = self.get_parent_article() @@ -171,7 +172,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: user_hash_int = int(user_hash[-4:], 16) user_profile_picture = get_profile_picture(user_hash_int) - if self.is_anonymous == 1: + if self.name_type == BoardNameType.ANONYMOUS: if parent_article_created_by_id == comment_created_by_id: user_name = gettext('author') else: @@ -187,7 +188,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: } } - if self.is_anonymous == 2: + if self.name_type == BoardNameType.REALNAME: sso_info = self.created_by.profile.sso_user_info user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index ddf8d8da..bb02446a 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -5,6 +5,7 @@ from rest_framework import serializers from apps.core.documents import ArticleDocument from apps.core.models import Article, Board, Block, Scrap, ArticleHiddenReason +from apps.core.models.board import BoardNameType from apps.core.serializers.board import BoardSerializer from apps.core.serializers.mixins.hidden import HiddenSerializerMixin, HiddenSerializerFieldMixin from apps.core.serializers.topic import TopicSerializer @@ -57,7 +58,7 @@ def get_content(self, obj) -> typing.Optional[str]: return None def get_created_by(self, obj) -> dict: - if obj.is_anonymous == 1 or obj.is_anonymous == 2: + if obj.name_type == BoardNameType.ANONYMOUS or obj.name_type == BoardNameType.REALNAME: return obj.postprocessed_created_by else: data = PublicUserSerializer(obj.postprocessed_created_by).data @@ -397,7 +398,7 @@ class ArticleUpdateActionSerializer(BaseArticleSerializer): class Meta(BaseArticleSerializer.Meta): exclude = ('migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) read_only_fields = ( - 'is_anonymous', + 'name_type', 'hit_count', 'comment_count', 'positive_vote_count', diff --git a/apps/core/serializers/comment.py b/apps/core/serializers/comment.py index f56639f0..60a0ad1f 100644 --- a/apps/core/serializers/comment.py +++ b/apps/core/serializers/comment.py @@ -1,5 +1,6 @@ from rest_framework import serializers import typing +from apps.core.models.board import BoardNameType from apps.core.serializers.mixins.hidden import HiddenSerializerFieldMixin, HiddenSerializerMixin from ara.classes.serializers import MetaDataModelSerializer @@ -33,7 +34,7 @@ def get_content(self, obj) -> typing.Optional[str]: return None def get_created_by(self, obj) -> dict: - if obj.is_anonymous == 1 or obj.is_anonymous == 2: + if obj.name_type == BoardNameType.ANONYMOUS or obj.name_type == BoardNameType.REALNAME: return obj.postprocessed_created_by else: data = PublicUserSerializer(obj.postprocessed_created_by).data @@ -108,7 +109,7 @@ class Meta(BaseCommentSerializer.Meta): class CommentUpdateActionSerializer(BaseCommentSerializer): class Meta(BaseCommentSerializer.Meta): read_only_fields = ( - 'is_anonymous', + 'name_type', 'positive_vote_count', 'negative_vote_count', 'created_by', diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index 2d2e1257..0d6a7c80 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext from rest_framework import status, viewsets, response, decorators, serializers, permissions from rest_framework.response import Response +from apps.core.models.board import BoardNameType from ara import redis from ara.classes.viewset import ActionAPIViewSet @@ -72,11 +73,13 @@ def filter_queryset(self, queryset): elif self.action == 'list': created_by = self.request.query_params.get('created_by') if created_by and int(created_by) != self.request.user.id: - queryset = queryset.exclude(is_anonymous=True) + # exclude someone's anonymous or realname article in one's profile + queryset = queryset.exclude(name_type=BoardNameType.ANONYMOUS).exclude(name_type=BoardNameType.REALNAME) + # exclude article written by blocked users in anonymous board queryset = queryset.exclude( created_by__id__in=self.request.user.block_set.values('user'), - is_anonymous=True + name_type=BoardNameType.ANONYMOUS ) # optimizing queryset for list action @@ -130,7 +133,7 @@ def filter_queryset(self, queryset): def perform_create(self, serializer): serializer.save( created_by=self.request.user, - is_anonymous=Board.objects.get(pk=self.request.data['parent_board']).is_anonymous + name_type=Board.objects.get(pk=self.request.data['parent_board']).name_type ) def update(self, request, *args, **kwargs): diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 7df16370..d70172d3 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext from rest_framework import mixins, status, response, decorators, serializers, permissions +from apps.core.models.board import BoardNameType from ara.classes.viewset import ActionAPIViewSet @@ -59,21 +60,21 @@ def create(self, request, *args, **kwargs): def perform_create(self, serializer): parent_article_id = self.request.data.get('parent_article') parent_article = parent_article_id and Article.objects.get(id=parent_article_id) - parent_article_is_anonymous = (parent_article and parent_article.is_anonymous) or 0 + parent_article_name_type = (parent_article and parent_article.name_type == BoardNameType.ANONYMOUS) or 0 parent_comment_id = self.request.data.get('parent_comment') parent_comment = parent_comment_id and Comment.objects.get(id=parent_comment_id) - parent_comment_is_anonymous = (parent_comment and parent_comment.is_anonymous) or 0 + parent_comment_name_type = (parent_comment and parent_comment.name_type == BoardNameType.ANONYMOUS) or 0 anonymous_status = 0 for stat in (1, 2): - if parent_article_is_anonymous == stat or parent_comment_is_anonymous == stat: + if parent_article_name_type == stat or parent_comment_name_type == stat: anonymous_status = stat break serializer.save( created_by=self.request.user, - is_anonymous=anonymous_status, + name_type=anonymous_status, ) def retrieve(self, request, *args, **kwargs): diff --git a/ara/locale/ko/LC_MESSAGES/django.po b/ara/locale/ko/LC_MESSAGES/django.po index d40f869e..b64d63ce 100644 --- a/ara/locale/ko/LC_MESSAGES/django.po +++ b/ara/locale/ko/LC_MESSAGES/django.po @@ -165,8 +165,7 @@ msgstr "기타 직원" #: apps/user/serializers/user_profile.py:43 #, python-format -msgid "" -"Nicknames can only be changed every 3 months. (can't change until %(date)s)" +msgid "Nicknames can only be changed every 3 months. (can't change until %(date)s)" msgstr "닉네임은 3개월마다 변경할 수 있습니다. (%(date)s까지 변경 불가)" #: ara/settings/django.py:96 diff --git a/tests/test_article_search.py b/tests/test_article_search.py index bc2b7896..400e45d3 100644 --- a/tests/test_article_search.py +++ b/tests/test_article_search.py @@ -3,6 +3,7 @@ from django.core.management import call_command from apps.core.models import Board, Article +from apps.core.models.board import BoardNameType from apps.user.models import UserProfile from tests.conftest import RequestSetting, TestCase @@ -50,7 +51,7 @@ def set_posts(request): title=f'게시물 {i+1}', content=article_content, content_text=article_content, - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, created_by=request.cls.authors[i % 4], diff --git a/tests/test_articles.py b/tests/test_articles.py index af753907..f1724bcb 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -5,6 +5,7 @@ from django.conf import settings from apps.core.models import Article, Topic, Board, Block, Vote, Comment +from apps.core.models.board import BoardNameType from apps.user.models import UserProfile from tests.conftest import RequestSetting, TestCase @@ -28,7 +29,7 @@ def set_anon_board(request): en_name="Anonymous", ko_description="익명 게시판", en_description="Anonymous", - is_anonymous=True + name_type=BoardNameType.ANONYMOUS ) @@ -52,7 +53,7 @@ def set_article(request): title="example article", content="example content", content_text="example content text", - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -87,7 +88,7 @@ def set_kaist_articles(request): title="example article", content="example content", content_text="example content text", - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -127,7 +128,7 @@ def test_list(self): title="example article", content="example content", content_text="example content text", - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -143,7 +144,7 @@ def test_list(self): title="example article", content="example content", content_text="example content text", - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -163,7 +164,7 @@ def test_get(self): res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data assert res.get('title') == self.article.title assert res.get('content') == self.article.content - assert res.get('is_anonymous') == self.article.is_anonymous + assert res.get('name_type') == self.article.name_type assert res.get('is_content_sexual') == self.article.is_content_sexual assert res.get('is_content_social') == self.article.is_content_social assert res.get('positive_vote_count') == self.article.positive_vote_count @@ -179,7 +180,7 @@ def test_anonymous_article(self): title="example anonymous article", content="example anonymous content", content_text="example anonymous content text", - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -193,11 +194,11 @@ def test_anonymous_article(self): # 익명 게시글을 GET할 때, 작성자의 정보가 전달되지 않는 것 확인 res = self.http_request(self.user, 'get', f'articles/{anon_article.id}').data - assert res.get('is_anonymous') + assert res.get('name_type') == BoardNameType.ANONYMOUS assert res.get('created_by')['username'] != anon_article.created_by.username res2 = self.http_request(self.user2, 'get', f'articles/{anon_article.id}').data - assert res2.get('is_anonymous') + assert res2.get('name_type') == BoardNameType.ANONYMOUS assert res2.get('created_by')['username'] != anon_article.created_by.username def test_create(self): @@ -207,7 +208,7 @@ def test_create(self): "title": "article for test_create", "content": "content for test_create", "content_text": "content_text for test_create", - "is_anonymous": False, + "name_type": BoardNameType.REGULAR, "is_content_sexual": False, "is_content_social": False, "parent_topic": self.topic.id, @@ -230,14 +231,14 @@ def test_create_anonymous(self): result = self.http_request(self.user, 'post', 'articles', user_data) - assert result.data['is_anonymous'] + assert result.data['name_type'] == BoardNameType.ANONYMOUS user_data.update({ "parent_topic": self.topic.id, "parent_board": self.board.id }) result = self.http_request(self.user, 'post', 'articles', user_data) - assert not result.data['is_anonymous'] + assert not result.data['name_type'] == BoardNameType.ANONYMOUS def test_update_cache_sync(self): new_title = 'title changed!' @@ -246,7 +247,7 @@ def test_update_cache_sync(self): title="example article", content="example content", content_text="example content text", - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -391,7 +392,7 @@ def test_readonly_board(self): "title": "article for test_create", "content": "content for test_create", "content_text": "content_text for test_create", - "is_anonymous": False, + "name_type": BoardNameType.REGULAR, "is_content_sexual": False, "is_content_social": False, "parent_board": self.readonly_board.id @@ -432,7 +433,7 @@ def test_deleting_with_comments(self): self.article.save() Comment.objects.create( content='this is a test comment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user, parent_article=self.article ) @@ -478,7 +479,7 @@ def _article_factory(self, is_content_sexual=False, is_content_social=False, **a title="example article", content="example content", content_text="example content text", - is_anonymous=False, + name_type=BoardNameType.REGULAR, hit_count=0, is_content_sexual=is_content_sexual, is_content_social=is_content_social, @@ -738,7 +739,7 @@ def test_comment_on_deleted_article(self): res = self.http_request(self.user, 'post', 'comments', { 'content': 'This is a comment', 'parent_article': target_article.id, - 'is_anonymous': False, + 'name_type': BoardNameType.REGULAR, }) assert res.status_code == 400 @@ -749,7 +750,7 @@ def test_comment_on_report_hidden_article(self): res = self.http_request(self.user, 'post', 'comments', { 'content': 'This is a comment', 'parent_article': target_article.id, - 'is_anonymous': False, + 'name_type': BoardNameType.REGULAR, }) assert res.status_code == 201 diff --git a/tests/test_block.py b/tests/test_block.py index 6dbf611d..12dd0d8f 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -5,6 +5,7 @@ from django.conf import settings from apps.core.models import Article, Topic, Board, Comment, Block +from apps.core.models.board import BoardNameType from tests.conftest import RequestSetting, TestCase from dateutil.relativedelta import relativedelta @@ -28,7 +29,7 @@ def set_anon_board(request): en_name="Anonymous", ko_description="익명 게시판", en_description="Anonymous", - is_anonymous=True + name_type=BoardNameType.ANONYMOUS ) @@ -52,7 +53,7 @@ def set_articles(request): title='Test Article', content='Content of test article', content_text='Content of test article in text', - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -68,7 +69,7 @@ def set_articles(request): title='Test Article 2', content='Content of test article 2', content_text='Content of test article in text 2', - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -84,7 +85,7 @@ def set_articles(request): title='Test Article 3', content='Content of test article 3', content_text='Content of test article in text 3', - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -225,7 +226,7 @@ def test_get_anonymous_articles_by_blocked_user(self): title='Test Article', content='Content of test article', content_text='Content of test article in text', - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -257,14 +258,14 @@ def test_comment_by_blocked_user(self): # user2가 댓글을 씀 blocked_comment = Comment.objects.create( content='this is a test comment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user2, parent_article=self.article, ) not_blocked_comment = Comment.objects.create( content='this is a test comment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user3, parent_article=self.article, ) diff --git a/tests/test_comments.py b/tests/test_comments.py index e5ff2579..d325e223 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import User from django.utils import timezone from apps.core.models import Article, Topic, Board, Comment, Block, Vote +from apps.core.models.board import BoardNameType from tests.conftest import RequestSetting, TestCase @@ -38,7 +39,7 @@ def set_articles(request): title='Test Article', content='Content of test article', content_text='Content of test article in text', - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -55,7 +56,7 @@ def set_articles(request): title='Anonymous Test Article', content='Content of test article', content_text='Content of test article in text', - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -73,14 +74,14 @@ def set_comments(request): """set_article 먼저 적용""" request.cls.comment = Comment.objects.create( content='this is a test comment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=request.cls.user, parent_article=request.cls.article, ) request.cls.comment_anonymous = Comment.objects.create( content='this is an anonymous test comment', - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, created_by=request.cls.user, parent_article=request.cls.article_anonymous, ) @@ -96,13 +97,13 @@ def test_comment_list(self): comment2 = Comment.objects.create( content='Test comment 2', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user, parent_article=self.article ) comment3 = Comment.objects.create( content='Test comment 3', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user, parent_article=self.article ) @@ -152,7 +153,7 @@ def test_article_comment_count_with_subcomments(self): subcomment1 = Comment.objects.create( content='Test comment 2', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user, parent_comment=self.comment ) @@ -163,7 +164,7 @@ def test_article_comment_count_with_subcomments(self): subcomment2 = Comment.objects.create( content='Test comment 3', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user, parent_comment=self.comment ) @@ -200,7 +201,7 @@ def test_delete_comment_by_not_comment_writer(self): def test_delete_comment_with_subcomment(self): subcomment = Comment.objects.create( content='Test subcomment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user, parent_comment=self.comment ) @@ -232,18 +233,18 @@ def test_anonymous_comment(self): # 익명 댓글 생성 anon_comment = Comment.objects.create( content='Anonymous test comment', - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, created_by=self.user, parent_article=self.article ) # 익명 댓글을 GET할 때, 작성자의 정보가 전달되지 않는 것 확인 res = self.http_request(self.user, 'get', f'comments/{anon_comment.id}').data - assert res.get('is_anonymous') + assert res.get('name_type') == BoardNameType.ANONYMOUS assert res.get('created_by')['username'] != anon_comment.created_by.username res2 = self.http_request(self.user2, 'get', f'comments/{anon_comment.id}').data - assert res2.get('is_anonymous') + assert res2.get('name_type') == BoardNameType.ANONYMOUS assert res2.get('created_by')['username'] != anon_comment.created_by.username # 익명글의 글쓴이가 본인의 글에 남긴 댓글에 대해, user_id가 같은지 확인 @@ -251,7 +252,7 @@ def test_anonymous_comment_by_article_writer(self): # 익명 댓글 생성 Comment.objects.create( content='Anonymous test comment', - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, created_by=self.user, parent_article=self.article ) @@ -277,7 +278,7 @@ def test_comment_on_anonymous_parent_article(self): 'attachment': None, } self.http_request(self.user, 'post', 'comments', comment_data) - assert Comment.objects.filter(content=comment_str).first().is_anonymous + assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS def test_comment_on_anonymous_parent_comment(self): comment_str = 'This is a comment on an anonymous parent comment' @@ -288,7 +289,7 @@ def test_comment_on_anonymous_parent_comment(self): 'attachment': None, } self.http_request(self.user, 'post', 'comments', comment_data) - assert Comment.objects.filter(content=comment_str).first().is_anonymous + assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS def test_comment_on_nonanonymous_parent_article(self): comment_str = 'This is a comment on an non-anonymous parent article' @@ -300,7 +301,7 @@ def test_comment_on_nonanonymous_parent_article(self): } self.http_request(self.user, 'post', 'comments', comment_data) Comment.objects.filter(content=comment_str).first() - assert not Comment.objects.filter(content=comment_str).first().is_anonymous + assert not Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS def test_comment_on_nonanonymous_parent_comment(self): comment_str = 'This is a comment on an non-anonymous parent comment' @@ -311,7 +312,7 @@ def test_comment_on_nonanonymous_parent_comment(self): 'attachment': None, } self.http_request(self.user, 'post', 'comments', comment_data) - assert not Comment.objects.filter(content=comment_str).first().is_anonymous + assert not Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS # 댓글 좋아요 확인 def test_positive_vote(self): @@ -363,7 +364,7 @@ class TestHiddenComments(TestCase, RequestSetting): def _comment_factory(self, **comment_kwargs): return Comment.objects.create( content='example comment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user, parent_article=self.article, **comment_kwargs @@ -387,7 +388,7 @@ def test_blocked_user_block(self): ) comment2 = Comment.objects.create( content='example comment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=self.user2, parent_article=self.article ) diff --git a/tests/test_recent.py b/tests/test_recent.py index 72695b24..89ab9d5d 100644 --- a/tests/test_recent.py +++ b/tests/test_recent.py @@ -4,6 +4,7 @@ from django.core.management import call_command from apps.core.models import Article, Topic, Board +from apps.core.models.board import BoardNameType from tests.conftest import RequestSetting, TestCase from django.utils import timezone @@ -40,7 +41,7 @@ def create_article(n): title=f'Test Article{n}', content=f'Content of test article {n}', content_text=f'Content_text of test article {n}', - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, diff --git a/tests/test_report.py b/tests/test_report.py index 14f453f7..b281f99e 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -1,6 +1,7 @@ import pytest from apps.core.models import Article, Topic, Board, Comment, Report from django.db.utils import IntegrityError +from apps.core.models.board import BoardNameType from tests.conftest import RequestSetting, TestCase from django.utils import timezone @@ -36,7 +37,7 @@ def set_article(request): title='Test Article', content='Content of test article', content_text='Content of test article in text', - is_anonymous=False, + name_type=BoardNameType.REGULAR, is_content_sexual=False, is_content_social=False, hit_count=0, @@ -54,7 +55,7 @@ def set_comment(request): """set_article 먼저 적용""" request.cls.comment = Comment.objects.create( content='this is a test comment', - is_anonymous=False, + name_type=BoardNameType.REGULAR, created_by=request.cls.user, parent_article=request.cls.article, ) diff --git a/tests/test_user.py b/tests/test_user.py index 5c9de477..7f5b314b 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -6,6 +6,7 @@ from django.core.validators import URLValidator from apps.core.models import Board, Article, Comment +from apps.core.models.board import BoardNameType from tests.conftest import RequestSetting, TestCase @@ -26,7 +27,7 @@ def set_articles(request): common_kwargs = { 'content': 'example content', 'content_text': 'example content text', - 'is_anonymous': False, + 'name_type': BoardNameType.REGULAR, 'created_by': request.cls.user2, 'parent_board': request.cls.board, 'hit_count': 0, @@ -75,7 +76,7 @@ def set_anonymous_article(request): title='익명글', is_content_sexual=False, is_content_social=False, - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, content='example content', content_text='example content text', created_by=request.cls.user2, @@ -89,7 +90,7 @@ def set_anonymous_comment(request): """set_anonymous_articles 먼저 적용""" request.cls.comment_anonymous = Comment.objects.create( content='example comment', - is_anonymous=True, + name_type=BoardNameType.ANONYMOUS, created_by=request.cls.user, parent_article=request.cls.article_anonymous, ) From ff8300f3656c3e2e3dd93e4ac022a877814d701d Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Tue, 5 Apr 2022 00:45:55 +0900 Subject: [PATCH 033/101] Fix perform_create name_type on REALNAME --- apps/core/views/viewsets/comment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index d70172d3..8ccbaa3f 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -60,14 +60,14 @@ def create(self, request, *args, **kwargs): def perform_create(self, serializer): parent_article_id = self.request.data.get('parent_article') parent_article = parent_article_id and Article.objects.get(id=parent_article_id) - parent_article_name_type = (parent_article and parent_article.name_type == BoardNameType.ANONYMOUS) or 0 + parent_article_name_type = parent_article and parent_article.name_type != BoardNameType.REGULAR parent_comment_id = self.request.data.get('parent_comment') parent_comment = parent_comment_id and Comment.objects.get(id=parent_comment_id) - parent_comment_name_type = (parent_comment and parent_comment.name_type == BoardNameType.ANONYMOUS) or 0 + parent_comment_name_type = parent_comment and parent_comment.name_type != BoardNameType.REGULAR anonymous_status = 0 - for stat in (1, 2): + for stat in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): if parent_article_name_type == stat or parent_comment_name_type == stat: anonymous_status = stat break From 32078f7c29ca32065f3a379bbacd04a0876a6c7e Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 5 Apr 2022 02:20:05 +0900 Subject: [PATCH 034/101] Remove `created_at` ordering --- apps/core/views/viewsets/communication_article.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index 017c1ef2..6898e054 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -13,7 +13,7 @@ class CommunicationArticleViewSet(viewsets.ReadOnlyModelViewSet, ActionAPIViewSe filter_backends = [filters.OrderingFilter, DjangoFilterBackend] # usage: /api/communication_articles/?ordering=created_at - ordering_fields = ['created_at', 'article__positive_vote_count'] + ordering_fields = ['article__positive_vote_count'] ordering = ['-article__positive_vote_count'] # default: 추천수 내림차순 # usage: /api/communication_articles/?school_response_status=1 From 1e7417bf5a5c0d5bff2d3541b590ffd5a974846d Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 5 Apr 2022 02:22:39 +0900 Subject: [PATCH 035/101] Add tests for communication article - test_school_response_only_increases - test_ordering_by_positive_vote_count - test_filtering_by_status --- tests/test_communication_article.py | 126 +++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 10 deletions(-) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index c82122e5..767ff353 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -1,13 +1,15 @@ import pytest + from django.utils import timezone from django.contrib.auth.models import User +from rest_framework.test import APIClient + from apps.core.models import Article, Board, Comment from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus from apps.user.models import UserProfile from tests.conftest import RequestSetting, TestCase -from rest_framework.test import APIClient @pytest.fixture(scope='class') @@ -45,14 +47,30 @@ def set_article(request): title='Article Title', content='Article Content', content_text='Article Content Text', - is_anonymous=False, - is_content_sexual=False, - is_content_social=False, - hit_count=0, - comment_count=0, - report_count=0, - positive_vote_count=0, - negative_vote_count=0, + created_by=request.cls.user, + parent_board=request.cls.board + ) + + +@pytest.fixture(scope='class') +def set_article1(request): + # After defining set_board + request.cls.article1 = Article.objects.create( + title='Article1 Title', + content='Article1 Content', + content_text='Article1 Content Text', + created_by=request.cls.user, + parent_board=request.cls.board + ) + + +@pytest.fixture(scope='class') +def set_article3(request): + # After defining set_board + request.cls.article3 = Article.objects.create( + title='Article3 Title', + content='Article3 Content', + content_text='Article3 Content Text', created_by=request.cls.user, parent_board=request.cls.board ) @@ -66,8 +84,25 @@ def set_communication_article(request): ) +@pytest.fixture(scope='class') +def set_communication_article1(request): + # After defining set_article1 + request.cls.communication_article1 = CommunicationArticle.objects.create( + article=request.cls.article1 + ) + + +@pytest.fixture(scope='class') +def set_communication_article3(request): + # After defining set_article3 + request.cls.communication_article3 = CommunicationArticle.objects.create( + article=request.cls.article3 + ) + + @pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', - 'set_school_admin', 'set_board', 'set_article', 'set_communication_article') + 'set_school_admin', 'set_board', 'set_article', 'set_article1', 'set_article3', + 'set_communication_article', 'set_communication_article1', 'set_communication_article3') class TestCommunicationArticle(TestCase, RequestSetting): # 소통 게시물이 communication_article 필드 가지는지 확인 def test_article_has_communication_article(self): @@ -121,3 +156,74 @@ def test_school_response_status(self): # answered_at 업데이트 확인 self.communication_article.refresh_from_db() assert self.communication_article.answered_at != min_time + + # school_response_status가 단조 증가하는지 확인 + def test_school_response_only_increases(self): + # status == 3 # SchoolResponseStatus.ANSWER_DONE + Comment.objects.create( + content = 'School Official Comment', + is_anonymous = False, + created_by = self.school_admin, + parent_article = self.article + ) + + # article에 좋아요 3개 추가해서 status 업데이트 시도 + users_tuple = (self.user2, self.user3, self.user4) + for user in users_tuple: + self.http_request(user, 'post', f'articles/{self.article.id}/vote_positive') + + # status 변화 없는지 확인 + res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data + assert res.get('communication_article_status') == SchoolResponseStatus.ANSWER_DONE + + # 좋아요 개수로 정렬 확인 + def test_ordering_by_positive_vote_count(self): + # 게시물에 좋아요 추가 + # [ article : 2개 / article1 : 3개 / article3 : 1개 ] + self.http_request(self.user2, 'post', f'articles/{self.article.id}/vote_positive') + self.http_request(self.user3, 'post', f'articles/{self.article.id}/vote_positive') + self.http_request(self.user2, 'post', f'articles/{self.article1.id}/vote_positive') + self.http_request(self.user3, 'post', f'articles/{self.article1.id}/vote_positive') + self.http_request(self.user4, 'post', f'articles/{self.article1.id}/vote_positive') + self.http_request(self.user2, 'post', f'articles/{self.article3.id}/vote_positive') + + # positive_vote_count 오름차순 + filtered_res = self.http_request(self.user, 'get', 'communication_articles', querystring='ordering=article__positive_vote_count').data.get('results') + filtered_mod = CommunicationArticle.objects.all().order_by('article__positive_vote_count') + for fr, fm in zip(filtered_res, filtered_mod): + assert fr['id'] == fm.id + + # positive_vote_count 내림차순 + filtered_res = self.http_request(self.user, 'get', 'communication_articles', querystring='ordering=-article__positive_vote_count').data.get('results') + filtered_mod = CommunicationArticle.objects.all().order_by('-article__positive_vote_count') + for fr, fm in zip(filtered_res, filtered_mod): + assert fr['id'] == fm.id + + # 답변 진행 상황 필터링 확인 + def test_filtering_by_status(self): + # article1 status == 1 # SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + self.http_request(self.user2, 'post', f'articles/{self.article1.id}/vote_positive') + self.http_request(self.user3, 'post', f'articles/{self.article1.id}/vote_positive') + self.http_request(self.user4, 'post', f'articles/{self.article1.id}/vote_positive') + + # article3 status == 3 # SchoolResponseStatus.ANSWER_DONE + Comment.objects.create( + content = 'School Official Comment', + is_anonymous = False, + created_by = self.school_admin, + parent_article = self.article3 + ) + + # Check filtering + status_tuple = ( + SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD, + SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM, + SchoolResponseStatus.ANSWER_DONE + ) + + for status in status_tuple: + querystring = f'school_response_status={status}' + filtered_res = self.http_request(self.user, 'get', 'communication_articles', querystring=querystring).data.get('results') + filtered_mod = CommunicationArticle.objects.filter(school_response_status=status) + assert len(filtered_res) == 1 and filtered_mod.count() == 1 + assert filtered_res[0]['school_response_status'] == filtered_mod.first().school_response_status From 82df058391066072a50185a3fb44d7e9683cd156 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 5 Apr 2022 03:38:26 +0900 Subject: [PATCH 036/101] Add all fields to CommunicationArticleSerializer --- apps/core/serializers/communication_article.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index 550b04bd..4002b159 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -1,12 +1,16 @@ +from rest_framework import serializers + from ara.classes.serializers import MetaDataModelSerializer from apps.core.models.communication_article import CommunicationArticle class CommunicationArticleSerializer(MetaDataModelSerializer): + positive_vote_count = serializers.IntegerField(source='article.positive_vote_count') + class Meta: model = CommunicationArticle - fields = [] - + fields = '__all__' + # TODO: is_confirmed_by_school 등을 timestamp 말고 progress_statues enum으로 바꿔서 프론트 보내주기 - # 좋아요 30개 달설 전, 달성 후 확인 전, 확인 후 답변전, 답변 완료 + # 좋아요 30개 달설 전, 달성 후 확인 전, 확인 후 답변전, 답변 완료 From 74ff771c7a271e8fa50ef278de96e11cfa305608 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Thu, 7 Apr 2022 23:41:36 +0900 Subject: [PATCH 037/101] Apply feedbacks --- apps/core/serializers/article.py | 2 +- apps/core/serializers/comment.py | 2 +- apps/core/views/viewsets/article.py | 3 ++- apps/core/views/viewsets/comment.py | 14 +++++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index bb02446a..a1c6997e 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -58,7 +58,7 @@ def get_content(self, obj) -> typing.Optional[str]: return None def get_created_by(self, obj) -> dict: - if obj.name_type == BoardNameType.ANONYMOUS or obj.name_type == BoardNameType.REALNAME: + if obj.name_type in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): return obj.postprocessed_created_by else: data = PublicUserSerializer(obj.postprocessed_created_by).data diff --git a/apps/core/serializers/comment.py b/apps/core/serializers/comment.py index 60a0ad1f..9e2b74dc 100644 --- a/apps/core/serializers/comment.py +++ b/apps/core/serializers/comment.py @@ -34,7 +34,7 @@ def get_content(self, obj) -> typing.Optional[str]: return None def get_created_by(self, obj) -> dict: - if obj.name_type == BoardNameType.ANONYMOUS or obj.name_type == BoardNameType.REALNAME: + if obj.name_type in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): return obj.postprocessed_created_by else: data = PublicUserSerializer(obj.postprocessed_created_by).data diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index 0d6a7c80..5c4ffcbb 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -74,7 +74,8 @@ def filter_queryset(self, queryset): created_by = self.request.query_params.get('created_by') if created_by and int(created_by) != self.request.user.id: # exclude someone's anonymous or realname article in one's profile - queryset = queryset.exclude(name_type=BoardNameType.ANONYMOUS).exclude(name_type=BoardNameType.REALNAME) + exclude_list = [BoardNameType.ANONYMOUS, BoardNameType.REALNAME] + queryset = queryset.exclude(name_type__in=exclude_list) # exclude article written by blocked users in anonymous board queryset = queryset.exclude( diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 8ccbaa3f..5bd08610 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -60,21 +60,21 @@ def create(self, request, *args, **kwargs): def perform_create(self, serializer): parent_article_id = self.request.data.get('parent_article') parent_article = parent_article_id and Article.objects.get(id=parent_article_id) - parent_article_name_type = parent_article and parent_article.name_type != BoardNameType.REGULAR + parent_article_name_type = parent_article and parent_article.name_type parent_comment_id = self.request.data.get('parent_comment') parent_comment = parent_comment_id and Comment.objects.get(id=parent_comment_id) - parent_comment_name_type = parent_comment and parent_comment.name_type != BoardNameType.REGULAR + parent_comment_name_type = parent_comment and parent_comment.name_type - anonymous_status = 0 - for stat in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): - if parent_article_name_type == stat or parent_comment_name_type == stat: - anonymous_status = stat + name_type = BoardNameType.REGULAR + for ntype in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): + if parent_article_name_type == ntype or parent_comment_name_type == ntype: + name_type = ntype break serializer.save( created_by=self.request.user, - name_type=anonymous_status, + name_type=name_type, ) def retrieve(self, request, *args, **kwargs): From a6c1e017ebc25c172dc14fdde105776f2543c8e6 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Fri, 8 Apr 2022 02:31:06 +0900 Subject: [PATCH 038/101] Add my_comment_profile field, minor fix --- apps/core/models/article.py | 3 +-- apps/core/models/comment.py | 7 +++++-- apps/core/serializers/article.py | 14 +++++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 7fa8ed0a..7444f226 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -216,7 +216,6 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: user_hash = hashlib.sha224(user_unique_encoding).hexdigest() user_hash_int = int(user_hash[-4:], 16) user_profile_picture = get_profile_picture(user_hash_int) - user_profile_picture_realname = get_profile_picture(self.created_by.id + HASH_SECRET_VALUE) if self.name_type == BoardNameType.ANONYMOUS: return { @@ -237,7 +236,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'id': user_unique_num, 'username': user_realname, 'profile': { - 'picture': default_storage.url(user_profile_picture_realname), + 'picture': default_storage.url(user_profile_picture), 'nickname': user_realname, 'user': user_realname }, diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index decacf02..50b2cebf 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -190,8 +190,11 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: if self.name_type == BoardNameType.REALNAME: sso_info = self.created_by.profile.sso_user_info - user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] - + if parent_article_created_by_id == comment_created_by_id: + user_realname = gettext('author') + else: + user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] + return { 'id': user_unique_num, 'username': user_realname, diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index a1c6997e..450961af 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext from rest_framework import serializers from apps.core.documents import ArticleDocument -from apps.core.models import Article, Board, Block, Scrap, ArticleHiddenReason +from apps.core.models import Article, Board, Block, Scrap, ArticleHiddenReason, Comment from apps.core.models.board import BoardNameType from apps.core.serializers.board import BoardSerializer from apps.core.serializers.mixins.hidden import HiddenSerializerMixin, HiddenSerializerFieldMixin @@ -271,6 +271,14 @@ def get_attachments(self, obj): # -> typing.Optional[list]: return obj.attachments.all().values_list('id') return None + def get_my_comment_profile(self, obj): + fake_comment = Comment(created_by = self.context['request'].user, name_type = obj.name_type, parent_article = obj) + if obj.name_type in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): + return fake_comment.postprocessed_created_by + else: + data = PublicUserSerializer(fake_comment.postprocessed_created_by).data + return data + parent_topic = TopicSerializer( read_only=True, ) @@ -282,6 +290,10 @@ def get_attachments(self, obj): # -> typing.Optional[list]: read_only=True, ) + my_comment_profile = serializers.SerializerMethodField( + read_only=True + ) + from apps.core.serializers.comment import ArticleNestedCommentListActionSerializer comments = ArticleNestedCommentListActionSerializer( many=True, From 634baf130056e7fc6098bbb43435cf15a3feaf63 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Sun, 24 Apr 2022 20:20:08 +0900 Subject: [PATCH 039/101] Apply feedback in comment name_type --- apps/core/views/viewsets/comment.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 5bd08610..52d15ba5 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -60,17 +60,10 @@ def create(self, request, *args, **kwargs): def perform_create(self, serializer): parent_article_id = self.request.data.get('parent_article') parent_article = parent_article_id and Article.objects.get(id=parent_article_id) - parent_article_name_type = parent_article and parent_article.name_type - parent_comment_id = self.request.data.get('parent_comment') parent_comment = parent_comment_id and Comment.objects.get(id=parent_comment_id) - parent_comment_name_type = parent_comment and parent_comment.name_type - name_type = BoardNameType.REGULAR - for ntype in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): - if parent_article_name_type == ntype or parent_comment_name_type == ntype: - name_type = ntype - break + name_type = parent_article.name_type if parent_article else parent_comment.name_type serializer.save( created_by=self.request.user, From d3ff4314b358763c8e980a3a1e81152d1c4ec4e6 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Sun, 24 Apr 2022 23:29:43 +0900 Subject: [PATCH 040/101] Fix typo - fix word auther to writer --- tests/test_comments.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_comments.py b/tests/test_comments.py index d325e223..50bd88a3 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -258,16 +258,16 @@ def test_anonymous_comment_by_article_writer(self): ) r_article = self.http_request(self.user, 'get', f'articles/{self.article_anonymous.id}').data - article_auther_id = r_article.get('created_by')['id'] + article_writer_id = r_article.get('created_by')['id'] r_comment = self.http_request(self.user, 'get', f'comments/{self.comment_anonymous.id}').data - comment_auther_id = r_comment.get('created_by')['id'] - assert article_auther_id == comment_auther_id + comment_writer_id = r_comment.get('created_by')['id'] + assert article_writer_id == comment_writer_id r_article2 = self.http_request(self.user2, 'get', f'articles/{self.article_anonymous.id}').data - article_auther_id2 = r_article2.get('created_by')['id'] + article_writer_id2 = r_article2.get('created_by')['id'] r_comment2 = self.http_request(self.user2, 'get', f'comments/{self.comment_anonymous.id}').data - comment_auther_id2 = r_comment2.get('created_by')['id'] - assert article_auther_id2 == comment_auther_id2 + comment_writer_id2 = r_comment2.get('created_by')['id'] + assert article_writer_id2 == comment_writer_id2 def test_comment_on_anonymous_parent_article(self): comment_str = 'This is a comment on an anonymous parent article' From c7582b14c0f8d9f81b158f2542e91a54dc5cfa7d Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Mon, 25 Apr 2022 20:14:28 +0900 Subject: [PATCH 041/101] Replace profile.is_official, is_school_admin with group --- .../0042_alter_board_access_mask.py | 18 +++++++++++ apps/core/models/board.py | 2 +- apps/core/models/signals/comment.py | 4 ++- .../core/permissions/communication_article.py | 11 +++++++ .../views/viewsets/communication_article.py | 7 ++--- .../migrations/0018_auto_20220424_2347.py | 31 +++++++++++++++++++ apps/user/models/user_profile.py | 24 ++++++-------- apps/user/serializers/user_profile.py | 15 +++++++++ tests/test_communication_article.py | 4 +-- 9 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 apps/core/migrations/0042_alter_board_access_mask.py create mode 100644 apps/core/permissions/communication_article.py create mode 100644 apps/user/migrations/0018_auto_20220424_2347.py diff --git a/apps/core/migrations/0042_alter_board_access_mask.py b/apps/core/migrations/0042_alter_board_access_mask.py new file mode 100644 index 00000000..dee95abc --- /dev/null +++ b/apps/core/migrations/0042_alter_board_access_mask.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.9 on 2022-04-24 14:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0041_communication_article_primary_key'), + ] + + operations = [ + migrations.AlterField( + model_name='board', + name='access_mask', + field=models.IntegerField(default=223, verbose_name='접근 권한 값'), + ), + ] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 681febbc..10cbe5cc 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -38,7 +38,7 @@ class Meta(MetaDataModel.Meta): # access_mask & (1< 0 일 때 접근이 가능합니다. # 사용자 그룹의 값들은 `UserGroup`을 참고하세요. access_mask = models.IntegerField( - default=2, # 카이스트 구성원만 사용 가능 + default=int('11011111', 2), # 카이스트 구성원만 사용 가능 null=False, verbose_name='접근 권한 값' ) diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index c4b90752..13c4bc08 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -4,6 +4,7 @@ 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) @@ -22,7 +23,8 @@ def update_article_commented_at(comment): def update_communication_article_status(comment): article = comment.parent_article if comment.parent_article else comment.parent_comment.parent_article - if article.parent_board.is_school_communication and comment.created_by.profile.is_school_admin: + if article.parent_board.is_school_communication and \ + comment.created_by.profile.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN: article.communication_article.answered_at = timezone.now() article.communication_article.school_response_status = SchoolResponseStatus.ANSWER_DONE article.communication_article.save() diff --git a/apps/core/permissions/communication_article.py b/apps/core/permissions/communication_article.py new file mode 100644 index 00000000..ad5d863d --- /dev/null +++ b/apps/core/permissions/communication_article.py @@ -0,0 +1,11 @@ +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 + diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index 70f6b83a..6944b390 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -3,6 +3,7 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status, filters, viewsets, response, permissions +from apps.core.permissions.communication_article import CommunicationArticleAdminPermission from ara.classes.viewset import ActionAPIViewSet from apps.core.models.communication_article import CommunicationArticle @@ -17,6 +18,7 @@ class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): } permission_classes = ( permissions.IsAuthenticated, + CommunicationArticleAdminPermission, ) filter_backends = [filters.OrderingFilter, DjangoFilterBackend] @@ -30,10 +32,7 @@ class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): # 학교 담당자가 신문고 게시글에 대해 `확인했습니다` 버튼을 누른 경우 def update(self, request, *args, **kwargs): # user가 학교 담당자인지 확인 - if not self.request.user.profile.is_school_admin: - return response.Response({'message': gettext('You are not authorized to access this feature')}, - status=status.HTTP_403_FORBIDDEN) - elif self.get_object().confirmed_by_school_at != timezone.datetime.min.replace(tzinfo=timezone.utc): + if self.get_object().confirmed_by_school_at != timezone.datetime.min.replace(tzinfo=timezone.utc): return response.Response(status=status.HTTP_200_OK) return super().update(request, *args, **kwargs) diff --git a/apps/user/migrations/0018_auto_20220424_2347.py b/apps/user/migrations/0018_auto_20220424_2347.py new file mode 100644 index 00000000..40cf5c3e --- /dev/null +++ b/apps/user/migrations/0018_auto_20220424_2347.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.9 on 2022-04-24 14:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0017_add_is-official_and_is-school-admin'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='is_official', + ), + migrations.RemoveField( + model_name='userprofile', + name='is_school_admin', + ), + migrations.AlterField( + model_name='manualuser', + name='org_type', + field=models.IntegerField(choices=[(0, 'Unauthorized user'), (1, 'KAIST member'), (2, 'Store employee'), (3, 'Other member'), (4, 'KAIST organization'), (5, 'External organization'), (6, 'Communication board admin'), (7, 'News board admin')], default=0), + ), + migrations.AlterField( + model_name='userprofile', + name='group', + field=models.IntegerField(choices=[(0, 'Unauthorized user'), (1, 'KAIST member'), (2, 'Store employee'), (3, 'Other member'), (4, 'KAIST organization'), (5, 'External organization'), (6, 'Communication board admin'), (7, 'News board admin')], default=0), + ), + ] diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index 6ced5aa8..21ffadb5 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -20,10 +20,16 @@ class Meta(MetaDataModel.Meta): ) class UserGroup(models.IntegerChoices): - UNAUTHORIZED = 0, ugettext_lazy('Unauthorized user') - KAIST_MEMBER = 1, ugettext_lazy('KAIST member') - FOOD_EMPLOYEE = 2, ugettext_lazy('Restaurant employee') - OTHER_EMPLOYEE = 3, ugettext_lazy('Other employee') + UNAUTHORIZED = 0, ugettext_lazy('Unauthorized user') # 뉴아라 계정을 만들지 않은 사람들 + KAIST_MEMBER = 1, ugettext_lazy('KAIST member') # 카이스트 메일을 가진 사람 (학생, 교직원) + STORE_EMPLOYEE = 2, ugettext_lazy('Store employee') # 교내 입주 업체 직원 + OTHER_MEMBER = 3, ugettext_lazy('Other member') # 카이스트 메일이 없는 개인 (특수한 관련자 등) + KAIST_ORG = 4, ugettext_lazy('KAIST organization') # 교내 학생 단체들 + EXTERNAL_ORG = 5, ugettext_lazy('External organization') # 외부인 (홍보 계정 등) + COMMUNICATION_BOARD_ADMIN = 6, ugettext_lazy('Communication board admin') # 소통게시판 관리인 + NEWS_BOARD_ADMIN = 7, ugettext_lazy('News board admin') # 뉴스게시판 관리인 + + OFFICIAL_GROUPS = [UserGroup.STORE_EMPLOYEE, UserGroup.KAIST_ORG] uid = models.CharField( null=True, @@ -112,16 +118,6 @@ class UserGroup(models.IntegerChoices): verbose_name='활동정지 마감 일시', ) - is_official = models.BooleanField( - default=False, - verbose_name='공식 계정', - ) - - is_school_admin = models.BooleanField( - default=False, - verbose_name='학교 관리자', - ) - def __str__(self) -> str: return self.nickname diff --git a/apps/user/serializers/user_profile.py b/apps/user/serializers/user_profile.py index 428a7838..6b71df03 100644 --- a/apps/user/serializers/user_profile.py +++ b/apps/user/serializers/user_profile.py @@ -65,6 +65,21 @@ class Meta(BaseUserProfileSerializer.Meta): 'is_school_admin', ) + is_official = serializers.SerializerMethodField( + read_only=True, + ) + is_school_admin = serializers.SerializerMethodField( + read_only=True, + ) + + @staticmethod + def get_is_official(obj) -> bool: + return obj.group in UserProfile.OFFICIAL_GROUPS + + @staticmethod + def get_is_school_admin(obj) -> bool: + return obj.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN + class MyPageUserProfileSerializer(BaseUserProfileSerializer): num_articles = serializers.SerializerMethodField() diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index c82122e5..ec9e63c7 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -21,7 +21,7 @@ def set_school_admin(request): user=request.cls.school_admin, nickname='School Admin', agree_terms_of_service_at=timezone.now(), - is_school_admin=True + group=UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN ) request.cls.api_client = APIClient() @@ -106,7 +106,7 @@ def test_school_response_status(self): self.communication_article.refresh_from_db() assert self.communication_article.response_deadline != min_time - # is_school_admin = True인 사용자가 코멘트 작성 + # group=COMMUNICATION_BOARD_ADMIN인 사용자가 코멘트 작성 Comment.objects.create( content = 'School Official Comment', is_anonymous = False, From 5ad32379011b399fd011fbca825e3507ff3e3257 Mon Sep 17 00:00:00 2001 From: kjy2844 Date: Mon, 25 Apr 2022 20:29:58 +0900 Subject: [PATCH 042/101] Squash user migration files --- ...017_add_is-official_and_is-school-admin.py | 23 ------------------- ...2347.py => 0017_add_user_group_options.py} | 12 ++-------- 2 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 apps/user/migrations/0017_add_is-official_and_is-school-admin.py rename apps/user/migrations/{0018_auto_20220424_2347.py => 0017_add_user_group_options.py} (72%) diff --git a/apps/user/migrations/0017_add_is-official_and_is-school-admin.py b/apps/user/migrations/0017_add_is-official_and_is-school-admin.py deleted file mode 100644 index e9fdc642..00000000 --- a/apps/user/migrations/0017_add_is-official_and_is-school-admin.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.9 on 2022-03-21 13:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('user', '0016_set_created_at_to_timezone_now'), - ] - - operations = [ - migrations.AddField( - model_name='userprofile', - name='is_official', - field=models.BooleanField(default=False, verbose_name='공식 계정'), - ), - migrations.AddField( - model_name='userprofile', - name='is_school_admin', - field=models.BooleanField(default=False, verbose_name='학교 관리자'), - ), - ] diff --git a/apps/user/migrations/0018_auto_20220424_2347.py b/apps/user/migrations/0017_add_user_group_options.py similarity index 72% rename from apps/user/migrations/0018_auto_20220424_2347.py rename to apps/user/migrations/0017_add_user_group_options.py index 40cf5c3e..80abcbee 100644 --- a/apps/user/migrations/0018_auto_20220424_2347.py +++ b/apps/user/migrations/0017_add_user_group_options.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.9 on 2022-04-24 14:47 +# Generated by Django 3.2.9 on 2022-04-25 11:22 from django.db import migrations, models @@ -6,18 +6,10 @@ class Migration(migrations.Migration): dependencies = [ - ('user', '0017_add_is-official_and_is-school-admin'), + ('user', '0016_set_created_at_to_timezone_now'), ] operations = [ - migrations.RemoveField( - model_name='userprofile', - name='is_official', - ), - migrations.RemoveField( - model_name='userprofile', - name='is_school_admin', - ), migrations.AlterField( model_name='manualuser', name='org_type', From 34cfe7c1d600b85cf45f80c5682e9d368fe5f174 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 26 Apr 2022 01:19:49 +0900 Subject: [PATCH 043/101] Add SchoolResponseStatus update tests --- .../core/serializers/communication_article.py | 3 - apps/core/views/viewsets/article.py | 1 - tests/test_communication_article.py | 354 ++++++++++-------- 3 files changed, 205 insertions(+), 153 deletions(-) diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index 4002b159..94a9af65 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -11,6 +11,3 @@ class CommunicationArticleSerializer(MetaDataModelSerializer): class Meta: model = CommunicationArticle fields = '__all__' - - # TODO: is_confirmed_by_school 등을 timestamp 말고 progress_statues enum으로 바꿔서 프론트 보내주기 - # 좋아요 30개 달설 전, 달성 후 확인 전, 확인 후 답변전, 답변 완료 diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index 01c57d6e..bae7a1fb 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -135,7 +135,6 @@ def perform_create(self, serializer): instance = serializer.instance if Board.objects.get(pk=self.request.data['parent_board']).is_school_communication: - # create communication article CommunicationArticle.objects.create( article=instance, ) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 767ff353..d3beeeab 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -29,8 +29,8 @@ def set_school_admin(request): @pytest.fixture(scope='class') -def set_board(request): - request.cls.board = Board.objects.create( +def set_communication_board(request): + request.cls.communication_board = Board.objects.create( slug='with-school', ko_name='학교와의 게시판 (테스트)', en_name='With School (Test)', @@ -41,38 +41,26 @@ def set_board(request): @pytest.fixture(scope='class') -def set_article(request): - # After defining set_board - request.cls.article = Article.objects.create( - title='Article Title', - content='Article Content', - content_text='Article Content Text', - created_by=request.cls.user, - parent_board=request.cls.board +def set_non_communication_board(request): + request.cls.non_communication_board = Board.objects.create( + slug='not with-school', + ko_name='학교 아닌 게시판 (테스트)', + en_name='Not With School (Test)', + ko_description='학교 아닌 게시판 (테스트)', + en_description='Not With School (Test)', + is_school_communication=False ) @pytest.fixture(scope='class') -def set_article1(request): - # After defining set_board - request.cls.article1 = Article.objects.create( - title='Article1 Title', - content='Article1 Content', - content_text='Article1 Content Text', - created_by=request.cls.user, - parent_board=request.cls.board - ) - - -@pytest.fixture(scope='class') -def set_article3(request): - # After defining set_board - request.cls.article3 = Article.objects.create( - title='Article3 Title', - content='Article3 Content', - content_text='Article3 Content Text', +def set_article(request): + # After defining set_communication_board + request.cls.article = Article.objects.create( + title='Communication Article Title', + content='Communication Article Content', + content_text='Communication Article Content Text', created_by=request.cls.user, - parent_board=request.cls.board + parent_board=request.cls.communication_board ) @@ -84,146 +72,214 @@ def set_communication_article(request): ) -@pytest.fixture(scope='class') -def set_communication_article1(request): - # After defining set_article1 - request.cls.communication_article1 = CommunicationArticle.objects.create( - article=request.cls.article1 - ) - +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', + 'set_school_admin', 'set_communication_board', 'set_non_communication_board', + 'set_article', 'set_communication_article') +class TestCommunicationArticle(TestCase, RequestSetting): -@pytest.fixture(scope='class') -def set_communication_article3(request): - # After defining set_article3 - request.cls.communication_article3 = CommunicationArticle.objects.create( - article=request.cls.article3 - ) + # ======================================================================= # + # Helper Functions # + # ======================================================================= # + def _get_communication_article_status(self, article): + res = self.http_request(self.user, 'get', f'articles/{article.id}').data + return res.get('communication_article_status') -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', - 'set_school_admin', 'set_board', 'set_article', 'set_article1', 'set_article3', - 'set_communication_article', 'set_communication_article1', 'set_communication_article3') -class TestCommunicationArticle(TestCase, RequestSetting): - # 소통 게시물이 communication_article 필드 가지는지 확인 - def test_article_has_communication_article(self): - assert self.article.parent_board.is_school_communication is True - assert hasattr(self.article, 'communication_article') + def _add_upvotes(self, article, users): + for user in users: + self.http_request(user, 'post', f'articles/{article.id}/vote_positive') - # 필드 디폴트 값 확인 - def test_default_fields(self): + def _add_admin_comment(self, article): + comment_str = 'Comment made in factory' + comment_data = { + 'content': comment_str, + 'created_by': self.school_admin.id, + 'parent_article': article.id + } + self.http_request(self.school_admin, 'post', 'comments', comment_data) + + # status를 가지는 communication_article 반환 + def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD): + article_title = f'Factory: Article Status {status}' + article_data = { + 'title': article_title, + 'content': 'Content made in factory', + 'content_text': 'Content Text made in factory', + 'created_by': self.user.id, + 'parent_board': self.communication_board.id, + } + self.http_request(self.user, 'post', 'articles', article_data) + + article = Article.objects.get(title=article_title) + + if status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: + pass + elif status == SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM: + users_tuple = (self.user2, self.user3, self.user4) + self._add_upvotes(article, users_tuple) + elif status == SchoolResponseStatus.PREPARING_ANSWER: + # 작성일 기준 status 변경 방법이 존재하지 않음 + article.communication_article.response_deadline = timezone.now() + article.communication_article.confirmed_by_school_at = timezone.now() + article.communication_article.school_response_status = SchoolResponseStatus.PREPARING_ANSWER + article.communication_article.save() + elif status == SchoolResponseStatus.ANSWER_DONE: + self._add_admin_comment(article) + + return article + + # ======================================================================= # + # Creation Test # + # ======================================================================= # + + # communication_article 생성 확인 + def test_create_communication_article(self): + # 소통 게시물 생성 + article_title = 'Article for test_create_communication_article' + user_data = { + 'title': article_title, + 'content': 'Content for test_create_communication_article', + 'content_text': 'test_create_communication_article', + 'creted_by': self.user.id, + 'parent_board': self.communication_board.id + } + self.http_request(self.user, 'post', 'articles', user_data) + + article = Article.objects.get(title=article_title) + + # 소통 게시물이 생성될 때 communication_article이 함께 생성되는지 확인 + article_id = article.id + communication_article = CommunicationArticle.objects.get(article_id=article_id) + assert communication_article + + # 필드 default 값 확인 min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) assert all([ - self.communication_article.response_deadline == min_time, - self.communication_article.confirmed_by_school_at == min_time, - self.communication_article.answered_at == min_time + communication_article.response_deadline == min_time, + communication_article.confirmed_by_school_at == min_time, + communication_article.answered_at == min_time, + communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD ]) + + # 소통 게시물이 아닌 게시물에 communication_article 없는지 확인 + def test_non_communication_article(self): + # 비소통 게시물 생성 + article_title = 'Article for test_non_communication_article' + user_data = { + 'title': article_title, + 'content': 'Content for test_non_communication_article', + 'content_text': 'test_non_communication_article', + 'creted_by': self.user.id, + 'parent_board': self.non_communication_board.id + } + self.http_request(self.user, 'post', 'articles', user_data) + + article_id = Article.objects.get(title=article_title).id + communication_article = CommunicationArticle.objects.filter(article_id=article_id).first() + assert communication_article is None + + # ======================================================================= # + # SchoolResponseStatus Update Test # + # ======================================================================= # + + # 0 -> 1 + def test_BEFORE_UPVOTE_THRESHOLD_to_BEFORE_SCHOOL_CONFIRM(self): + from_stauts = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD + to_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + + article = self._create_article_with_status(from_stauts) + assert self._get_communication_article_status(article) == from_stauts - # school_response_status 업데이트 확인 - def test_school_response_status(self): - # vote 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD보다 작으면 status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD - res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data - assert res.get('communication_article_status') == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD - - # 게시물에 좋아요 3개 추가 (작성일 기준 SCHOOL_RESPONSE_VOTE_THRESHOLD == 3) users_tuple = (self.user2, self.user3, self.user4) - for user in users_tuple: - self.http_request(user, 'post', f'articles/{self.article.id}/vote_positive') + self._add_upvotes(article, users_tuple) + assert self._get_communication_article_status(article) == to_status + + # 0 -> 2 + def test_BEFORE_UPVOTE_THRESHOLD_to_PREPARING_ANSWER(self): + # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 + pass - # 좋아요 개수 업데이트 확인 - res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data - assert res.get('positive_vote_count') == len(users_tuple) + # 0 -> 3 + def test_BEFORE_UPVOTE_THRESHOLD_to_ANSWER_DONE(self): + from_status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD + to_status = SchoolResponseStatus.ANSWER_DONE - # 좋아요 개수가 SCHOOL_RESPONSE_VOTE_THRESHOLD 이상이므로 status == SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM - assert res.get('communication_article_status') == SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status - # response_deadline 업데이트 확인 - min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) - self.communication_article.refresh_from_db() - assert self.communication_article.response_deadline != min_time - - # is_school_admin = True인 사용자가 코멘트 작성 - Comment.objects.create( - content = 'School Official Comment', - is_anonymous = False, - created_by = self.school_admin, - parent_article = self.article - ) + self._add_admin_comment(article) + assert self._get_communication_article_status(article) == to_status + + # 1 -> 2 + def test_BEFORE_SCHOOL_CONFIRM_to_PREPARING_ANSWER(self): + # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 + pass - # school admin이 코멘트 작성했으므로 status == SchoolResponseStatus.ANSWER_DONE - res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data - assert res.get('communication_article_status') == SchoolResponseStatus.ANSWER_DONE - - # answered_at 업데이트 확인 - self.communication_article.refresh_from_db() - assert self.communication_article.answered_at != min_time - - # school_response_status가 단조 증가하는지 확인 - def test_school_response_only_increases(self): - # status == 3 # SchoolResponseStatus.ANSWER_DONE - Comment.objects.create( - content = 'School Official Comment', - is_anonymous = False, - created_by = self.school_admin, - parent_article = self.article - ) + # 1 -> 3 + def test_BEFORE_SCHOOL_CONFIRM_to_ANSWER_DONE(self): + from_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + to_status = SchoolResponseStatus.ANSWER_DONE + + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status + + self._add_admin_comment(article) + assert self._get_communication_article_status(article) == to_status + + # 2 -> 3 + def test_PREPARING_ANSWER_to_ANSWER_DONE(self): + from_status = SchoolResponseStatus.PREPARING_ANSWER + to_status = SchoolResponseStatus.ANSWER_DONE + + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status + + self._add_admin_comment(article) + assert self._get_communication_article_status(article) == to_status + + # 2 -> 1 (관리자가 '답변 준비 중' 취소하는 경우) + def test_PREPARING_ANSWER_to_BEFORE_SCHOOL_CONFIRM(self): + # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 + pass + + # 3 -> 1 + def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): + status = SchoolResponseStatus.ANSWER_DONE + + article = self._create_article_with_status(status) + assert self._get_communication_article_status(article) == status - # article에 좋아요 3개 추가해서 status 업데이트 시도 users_tuple = (self.user2, self.user3, self.user4) - for user in users_tuple: - self.http_request(user, 'post', f'articles/{self.article.id}/vote_positive') - - # status 변화 없는지 확인 - res = self.http_request(self.user, 'get', f'articles/{self.article.id}').data - assert res.get('communication_article_status') == SchoolResponseStatus.ANSWER_DONE + self._add_upvotes(article, users_tuple) + assert self._get_communication_article_status(article) == status + + # 3 -> 2 + def test_ANSWER_DONE_to_PREPARING_ANSWER(self): + # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 + pass + + # ======================================================================= # + # Ordering & Filtering # + # ======================================================================= # # 좋아요 개수로 정렬 확인 def test_ordering_by_positive_vote_count(self): - # 게시물에 좋아요 추가 - # [ article : 2개 / article1 : 3개 / article3 : 1개 ] - self.http_request(self.user2, 'post', f'articles/{self.article.id}/vote_positive') - self.http_request(self.user3, 'post', f'articles/{self.article.id}/vote_positive') - self.http_request(self.user2, 'post', f'articles/{self.article1.id}/vote_positive') - self.http_request(self.user3, 'post', f'articles/{self.article1.id}/vote_positive') - self.http_request(self.user4, 'post', f'articles/{self.article1.id}/vote_positive') - self.http_request(self.user2, 'post', f'articles/{self.article3.id}/vote_positive') - - # positive_vote_count 오름차순 - filtered_res = self.http_request(self.user, 'get', 'communication_articles', querystring='ordering=article__positive_vote_count').data.get('results') - filtered_mod = CommunicationArticle.objects.all().order_by('article__positive_vote_count') - for fr, fm in zip(filtered_res, filtered_mod): - assert fr['id'] == fm.id - - # positive_vote_count 내림차순 - filtered_res = self.http_request(self.user, 'get', 'communication_articles', querystring='ordering=-article__positive_vote_count').data.get('results') - filtered_mod = CommunicationArticle.objects.all().order_by('-article__positive_vote_count') - for fr, fm in zip(filtered_res, filtered_mod): - assert fr['id'] == fm.id + # TODO: In different branch + pass # 답변 진행 상황 필터링 확인 def test_filtering_by_status(self): - # article1 status == 1 # SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM - self.http_request(self.user2, 'post', f'articles/{self.article1.id}/vote_positive') - self.http_request(self.user3, 'post', f'articles/{self.article1.id}/vote_positive') - self.http_request(self.user4, 'post', f'articles/{self.article1.id}/vote_positive') - - # article3 status == 3 # SchoolResponseStatus.ANSWER_DONE - Comment.objects.create( - content = 'School Official Comment', - is_anonymous = False, - created_by = self.school_admin, - parent_article = self.article3 - ) + # TODO: In different branch + pass - # Check filtering - status_tuple = ( - SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD, - SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM, - SchoolResponseStatus.ANSWER_DONE - ) + # ======================================================================= # + # Anonymous # + # ======================================================================= # + + # 익명 게시물 작성 불가 확인 + def test_anonymous_article(self): + pass - for status in status_tuple: - querystring = f'school_response_status={status}' - filtered_res = self.http_request(self.user, 'get', 'communication_articles', querystring=querystring).data.get('results') - filtered_mod = CommunicationArticle.objects.filter(school_response_status=status) - assert len(filtered_res) == 1 and filtered_mod.count() == 1 - assert filtered_res[0]['school_response_status'] == filtered_mod.first().school_response_status + # 익명 댓글 작성 불가 확인 + def test_anonymous_comment(self): + pass From b840e4476bfd1c31e465903c362a47700f69950e Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 26 Apr 2022 01:33:56 +0900 Subject: [PATCH 044/101] Add anonymous article & comment tests --- tests/test_communication_article.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index d3beeeab..d773b8b7 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -90,9 +90,8 @@ def _add_upvotes(self, article, users): self.http_request(user, 'post', f'articles/{article.id}/vote_positive') def _add_admin_comment(self, article): - comment_str = 'Comment made in factory' comment_data = { - 'content': comment_str, + 'content': 'Comment made in factory', 'created_by': self.school_admin.id, 'parent_article': article.id } @@ -278,8 +277,25 @@ def test_filtering_by_status(self): # 익명 게시물 작성 불가 확인 def test_anonymous_article(self): - pass + article_title = 'Anonymous article' + article_data = { + 'title': article_title, + 'content': 'Content of anonymous article', + 'content_text': 'Content text of anonymous article', + 'created_by': self.user.id, + 'parent_board': self.communication_board.id, + 'is_anonymous': True + } + res = self.http_request(self.user, 'post', 'articles', article_data).data + assert res.get('is_anonymous') == False # 익명 댓글 작성 불가 확인 def test_anonymous_comment(self): - pass + comment_data = { + 'content': 'Anonymous comment', + 'created_by': self.school_admin.id, + 'parent_article': self.article.id, + 'is_anonymous': True + } + res = self.http_request(self.user, 'post', 'comments', comment_data).data + assert res.get('is_anonymous') == False From 0326ea0c1eaa2c222d877f273b46bfc877f15b6b Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 26 Apr 2022 03:10:43 +0900 Subject: [PATCH 045/101] Resolve conflicts --- .../migrations/0041_merge_20220426_0146.py | 14 ++++++++++ tests/conftest.py | 13 ++++++--- tests/test_communication_article.py | 27 ++++++++++++------- 3 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 apps/core/migrations/0041_merge_20220426_0146.py diff --git a/apps/core/migrations/0041_merge_20220426_0146.py b/apps/core/migrations/0041_merge_20220426_0146.py new file mode 100644 index 00000000..f304dac5 --- /dev/null +++ b/apps/core/migrations/0041_merge_20220426_0146.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2.9 on 2022-04-25 16:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0038_rename_is_anonymous_to_name_type'), + ('core', '0040_communicationarticle_school_response_status'), + ] + + operations = [ + ] diff --git a/tests/conftest.py b/tests/conftest.py index 7ea2e47b..17d789ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,10 +25,17 @@ def set_admin_client(request): @pytest.fixture(scope='class') def set_user_client(request): - request.cls.user, _ = User.objects.get_or_create(username='User', email='user@sparcs.org') + request.cls.user, _ = User.objects.get_or_create( + username='User', + email='user@sparcs.org', + ) if not hasattr(request.cls.user, 'profile'): - UserProfile.objects.get_or_create(user=request.cls.user, nickname='User', - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) + UserProfile.objects.get_or_create( + user=request.cls.user, + nickname='User', + group=UserProfile.UserGroup.KAIST_MEMBER, + agree_terms_of_service_at=timezone.now(), + ) client = APIClient() request.cls.api_client = client diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index d773b8b7..af9f0201 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -5,7 +5,8 @@ from rest_framework.test import APIClient -from apps.core.models import Article, Board, Comment +from apps.core.models import Article, Board +from apps.core.models.board import BoardNameType from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus from apps.user.models import UserProfile @@ -23,7 +24,12 @@ def set_school_admin(request): user=request.cls.school_admin, nickname='School Admin', agree_terms_of_service_at=timezone.now(), - is_school_admin=True + is_school_admin=True, + sso_user_info={ + 'kaist_info': '{\"ku_kname\": \"\\ud669\"}', + 'first_name': 'FirstName', + 'last_name': 'LastName' + } ) request.cls.api_client = APIClient() @@ -36,7 +42,8 @@ def set_communication_board(request): en_name='With School (Test)', ko_description='학교와의 게시판 (테스트)', en_description='With School (Test)', - is_school_communication=True + is_school_communication=True, + name_type=BoardNameType.REALNAME ) @@ -60,7 +67,8 @@ def set_article(request): content='Communication Article Content', content_text='Communication Article Content Text', created_by=request.cls.user, - parent_board=request.cls.communication_board + parent_board=request.cls.communication_board, + name_type=BoardNameType.REALNAME ) @@ -106,6 +114,7 @@ def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_ 'content_text': 'Content Text made in factory', 'created_by': self.user.id, 'parent_board': self.communication_board.id, + 'name_type': BoardNameType.REALNAME } self.http_request(self.user, 'post', 'articles', article_data) @@ -284,18 +293,18 @@ def test_anonymous_article(self): 'content_text': 'Content text of anonymous article', 'created_by': self.user.id, 'parent_board': self.communication_board.id, - 'is_anonymous': True + 'name_type': BoardNameType.ANONYMOUS } res = self.http_request(self.user, 'post', 'articles', article_data).data - assert res.get('is_anonymous') == False + assert res.get('name_type') == BoardNameType.REALNAME # 익명 댓글 작성 불가 확인 def test_anonymous_comment(self): comment_data = { 'content': 'Anonymous comment', - 'created_by': self.school_admin.id, + 'created_by': self.user.id, 'parent_article': self.article.id, - 'is_anonymous': True + 'name_type': BoardNameType.ANONYMOUS } res = self.http_request(self.user, 'post', 'comments', comment_data).data - assert res.get('is_anonymous') == False + assert res.get('name_type') == BoardNameType.REALNAME From 3a3990d1bfa0570a0b62a6cf6a0aca88bf9915f7 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 27 Apr 2022 01:59:20 +0900 Subject: [PATCH 046/101] Add sso_info to user --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 17d789ac..51d6f929 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,11 @@ def set_user_client(request): nickname='User', group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now(), + sso_user_info={ + 'kaist_info': '{\"ku_kname\": \"\\ud669\"}', + 'first_name': 'FirstName', + 'last_name': 'LastName' + } ) client = APIClient() request.cls.api_client = client From 56331b72cfbb4059987df24b65113048c3a8a9bd Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Mon, 25 Apr 2022 22:37:42 +0900 Subject: [PATCH 047/101] Make get_realname function --- apps/core/models/article.py | 3 +-- apps/core/models/comment.py | 3 +-- apps/user/models/user_profile.py | 8 ++++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 7444f226..0ca21753 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -229,8 +229,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: } if self.name_type == BoardNameType.REALNAME: - sso_info = self.created_by.profile.sso_user_info - user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] + user_realname = self.created_by.profile.get_realname return { 'id': user_unique_num, diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index 50b2cebf..a1a35a11 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -189,11 +189,10 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: } if self.name_type == BoardNameType.REALNAME: - sso_info = self.created_by.profile.sso_user_info if parent_article_created_by_id == comment_created_by_id: user_realname = gettext('author') else: - user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] + user_realname = self.created_by.profile.get_realname return { 'id': user_unique_num, diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index 6ced5aa8..287c99cd 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -1,3 +1,5 @@ +import json + from cached_property import cached_property from dateutil.relativedelta import relativedelta from django.db import models @@ -132,3 +134,9 @@ def can_change_nickname(self) -> bool: def email(self) -> str: return self.user.email + @cached_property + def get_realname(self) -> str: + sso_info = self.sso_user_info + user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] + + return user_realname From 0f727b5a0b4d81cfbcba66230bdac1f84f4c37c0 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Thu, 28 Apr 2022 18:09:21 +0900 Subject: [PATCH 048/101] Add tests for realname article and comment --- tests/conftest.py | 11 ++-- tests/test_articles.py | 72 +++++++++++++++++++++++++- tests/test_comments.py | 113 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 182 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7ea2e47b..2855059e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,40 +22,45 @@ def set_admin_client(request): client.force_authenticate(user=request.cls.user) request.cls.api_client = client - +# sso_user_info에 kaist_info 있음 @pytest.fixture(scope='class') def set_user_client(request): request.cls.user, _ = User.objects.get_or_create(username='User', email='user@sparcs.org') if not hasattr(request.cls.user, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user, nickname='User', + sso_user_info={"kaist_info": "{\"ku_kname\": \"user\"}"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) client = APIClient() request.cls.api_client = client - +# sso_user_info에 kaist_info 있음 @pytest.fixture(scope='class') def set_user_client2(request): request.cls.user2, _ = User.objects.get_or_create(username='User2', email='user2@sparcs.org') if not hasattr(request.cls.user2, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user2, nickname='User2', + sso_user_info={"kaist_info": "{\"ku_kname\": \"user2\"}"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) request.cls.api_client = APIClient() - +# sso_user_info에 kaist_info 없음 @pytest.fixture(scope='class') def set_user_client3(request): request.cls.user3, _ = User.objects.get_or_create(username='User3', email='user3@sparcs.org') if not hasattr(request.cls.user3, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user3, nickname='User3', + sso_user_info={"kaist_info": None, "last_name": "User", "first_name": "3"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) request.cls.api_client = APIClient() +# sso_user_info에 kaist_info 없음 @pytest.fixture(scope='class') def set_user_client4(request): request.cls.user4, _ = User.objects.get_or_create(username='User4', email='user4@sparcs.org') if not hasattr(request.cls.user4, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user4, nickname='User4', + sso_user_info={"kaist_info": None, "last_name": "User", "first_name": "4"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) request.cls.api_client = APIClient() diff --git a/tests/test_articles.py b/tests/test_articles.py index f1724bcb..e12c7761 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -33,6 +33,18 @@ def set_anon_board(request): ) +@pytest.fixture(scope='class') +def set_realname_board(request): + request.cls.realname_board = Board.objects.create( + slug="realname", + ko_name="실명 게시판", + en_name="Realname Board", + ko_description="실명 게시판", + en_description="Realname Board", + name_type=BoardNameType.REALNAME + ) + + @pytest.fixture(scope='class') def set_topic(request): """set_board 먼저 적용""" @@ -117,7 +129,7 @@ def set_readonly_board(request): request.cls.readonly_board.delete() -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_board', 'set_anon_board', 'set_topic', 'set_article') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3','set_user_client4','set_board', 'set_anon_board', 'set_realname_board', 'set_topic', 'set_article') class TestArticle(TestCase, RequestSetting): def test_list(self): # article 개수를 확인하는 테스트 @@ -201,6 +213,42 @@ def test_anonymous_article(self): assert res2.get('name_type') == BoardNameType.ANONYMOUS assert res2.get('created_by')['username'] != anon_article.created_by.username + # http get으로 익명 게시글을 retrieve했을 때 작성자가 실명으로 나타나는지 확인 + def test_realname_article(self): + # 실명 게시글 생성 + realname_article = Article.objects.create( + title="example realname article", + content="example realname content", + content_text="example realname content text", + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=self.user, + parent_topic=None, + parent_board=self.realname_board, + commented_at=timezone.now() + ) + + # 익명 게시글을 GET할 때, 작성자의 정보가 실명으로 전달되는 것 확인 + res = self.http_request(self.user, 'get', f'articles/{realname_article.id}').data + assert res.get('name_type') == BoardNameType.REALNAME + assert res.get('created_by')['username'] == realname_article.created_by.profile.get_realname + + res2 = self.http_request(self.user2, 'get', f'articles/{realname_article.id}').data + assert res2.get('name_type') == BoardNameType.REALNAME + assert res2.get('created_by')['username'] == realname_article.created_by.profile.get_realname + + res3 = self.http_request(self.user3, 'get', f'articles/{realname_article.id}').data + assert res3.get('name_type') == BoardNameType.REALNAME + assert res3.get('created_by')['username'] == realname_article.created_by.profile.get_realname + + res4 = self.http_request(self.user4, 'get', f'articles/{realname_article.id}').data + assert res4.get('name_type') == BoardNameType.REALNAME + assert res4.get('created_by')['username'] == realname_article.created_by.profile.get_realname + def test_create(self): # test_create: HTTP request (POST)를 이용해서 생성 # user data in dict @@ -240,6 +288,28 @@ def test_create_anonymous(self): result = self.http_request(self.user, 'post', 'articles', user_data) assert not result.data['name_type'] == BoardNameType.ANONYMOUS + def test_create_realname(self): + user_data = { + "title": "article for test_create", + "content": "content for test_create", + "content_text": "content_text for test_create", + "is_content_sexual": False, + "is_content_social": False, + "parent_topic": None, + "parent_board": self.realname_board.id + } + + result = self.http_request(self.user, 'post', 'articles', user_data) + + assert result.data['name_type'] == BoardNameType.REALNAME + + user_data.update({ + "parent_topic": self.topic.id, + "parent_board": self.board.id + }) + result = self.http_request(self.user, 'post', 'articles', user_data) + assert not result.data['name_type'] == BoardNameType.REALNAME + def test_update_cache_sync(self): new_title = 'title changed!' new_content = 'content changed!' diff --git a/tests/test_comments.py b/tests/test_comments.py index 50bd88a3..5e18d12f 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -68,6 +68,22 @@ def set_articles(request): commented_at=timezone.now() ) + request.cls.article_realname = Article.objects.create( + title='Realname Test Article', + content='Content of test article', + content_text='Content of test article in text', + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=request.cls.user, + parent_topic=request.cls.topic, + parent_board=request.cls.board, + commented_at=timezone.now() + ) + @pytest.fixture(scope='class') def set_comments(request): @@ -86,8 +102,15 @@ def set_comments(request): parent_article=request.cls.article_anonymous, ) + request.cls.comment_realname = Comment.objects.create( + content='this is an realname test comment', + name_type=BoardNameType.REALNAME, + created_by=request.cls.user, + parent_article=request.cls.article_realname, + ) + -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_board', 'set_topic', 'set_articles', 'set_comments') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', 'set_board', 'set_topic', 'set_articles', 'set_comments') class TestComments(TestCase, RequestSetting): # comment 개수를 확인하는 테스트 def test_comment_list(self): @@ -269,6 +292,77 @@ def test_anonymous_comment_by_article_writer(self): comment_writer_id2 = r_comment2.get('created_by')['id'] assert article_writer_id2 == comment_writer_id2 + # http get으로 실명 댓글을 retrieve했을 때 작성자가 실명으로 나타나는지 확인 + def test_realname_comment(self): + # 실명 댓글 생성 + realname_comment = Comment.objects.create( + content='Realname test comment', + name_type=BoardNameType.REALNAME, + created_by=self.user, + parent_article=self.article + ) + + # 익명 댓글을 GET할 때, 작성자의 정보가 전달되지 않는 것 확인 + res = self.http_request(self.user, 'get', f'comments/{realname_comment.id}').data + assert res.get('name_type') == BoardNameType.REALNAME + assert res.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + + res2 = self.http_request(self.user2, 'get', f'comments/{realname_comment.id}').data + assert res2.get('name_type') == BoardNameType.REALNAME + assert res2.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + + res3 = self.http_request(self.user3, 'get', f'comments/{realname_comment.id}').data + assert res3.get('name_type') == BoardNameType.REALNAME + assert res3.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + + res4 = self.http_request(self.user4, 'get', f'comments/{realname_comment.id}').data + assert res4.get('name_type') == BoardNameType.REALNAME + assert res4.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + + def test_realname_comment_by_article_writer(self): + # 익명 댓글 생성 + Comment.objects.create( + content='Anonymous test comment', + name_type=BoardNameType.REALNAME, + created_by=self.user, + parent_article=self.article + ) + + r_article = self.http_request(self.user, 'get', f'articles/{self.article_realname.id}').data + article_writer_id = r_article.get('created_by')['id'] + r_comment = self.http_request(self.user, 'get', f'comments/{self.comment_realname.id}').data + comment_writer_id = r_comment.get('created_by')['id'] + assert article_writer_id == comment_writer_id + + r_article2 = self.http_request(self.user2, 'get', f'articles/{self.article_realname.id}').data + article_writer_id2 = r_article2.get('created_by')['id'] + r_comment2 = self.http_request(self.user2, 'get', f'comments/{self.comment_realname.id}').data + comment_writer_id2 = r_comment2.get('created_by')['id'] + assert article_writer_id2 == comment_writer_id2 + + def test_comment_on_regular_parent_article(self): + comment_str = 'This is a comment on a regular parent article' + comment_data = { + 'content': comment_str, + 'parent_article': self.article.id, + 'parent_comment': None, + 'attachment': None, + } + self.http_request(self.user, 'post', 'comments', comment_data) + Comment.objects.filter(content=comment_str).first() + assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.REGULAR + + def test_comment_on_regular_parent_comment(self): + comment_str = 'This is a comment on a regular parent comment' + comment_data = { + 'content': comment_str, + 'parent_article': None, + 'parent_comment': self.comment.id, + 'attachment': None, + } + self.http_request(self.user, 'post', 'comments', comment_data) + assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.REGULAR + def test_comment_on_anonymous_parent_article(self): comment_str = 'This is a comment on an anonymous parent article' comment_data = { @@ -291,28 +385,27 @@ def test_comment_on_anonymous_parent_comment(self): self.http_request(self.user, 'post', 'comments', comment_data) assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS - def test_comment_on_nonanonymous_parent_article(self): - comment_str = 'This is a comment on an non-anonymous parent article' + def test_comment_on_realname_parent_article(self): + comment_str = 'This is a comment on a realname parent article' comment_data = { 'content': comment_str, - 'parent_article': self.article.id, + 'parent_article': self.article_realname.id, 'parent_comment': None, 'attachment': None, } self.http_request(self.user, 'post', 'comments', comment_data) - Comment.objects.filter(content=comment_str).first() - assert not Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS + assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.REALNAME - def test_comment_on_nonanonymous_parent_comment(self): - comment_str = 'This is a comment on an non-anonymous parent comment' + def test_comment_on_realname_parent_comment(self): + comment_str = 'This is a comment on a realname parent comment' comment_data = { 'content': comment_str, 'parent_article': None, - 'parent_comment': self.comment.id, + 'parent_comment': self.comment_realname.id, 'attachment': None, } self.http_request(self.user, 'post', 'comments', comment_data) - assert not Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS + assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.REALNAME # 댓글 좋아요 확인 def test_positive_vote(self): From 095a77c0b526c89eecdfc4fa84f11515ad0e70ce Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Fri, 29 Apr 2022 01:08:52 +0900 Subject: [PATCH 049/101] Add days_left for Communication Article admin --- apps/core/serializers/article.py | 2 +- .../core/serializers/communication_article.py | 25 +++++++++++++------ .../views/viewsets/communication_article.py | 6 +++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 7c98acb8..ecf9d332 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -8,7 +8,7 @@ from apps.core.documents import ArticleDocument from apps.core.models import Article, Board, Block, Scrap, ArticleHiddenReason from apps.core.serializers.board import BoardSerializer -from apps.core.serializers.communication_article import CommunicationArticleSerializer +from apps.core.serializers.communication_article import BaseCommunicationArticleSerializer from apps.core.serializers.mixins.hidden import HiddenSerializerMixin, HiddenSerializerFieldMixin from apps.core.serializers.topic import TopicSerializer from apps.user.serializers.user import PublicUserSerializer diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index 5d5f9abe..38fa0e96 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -1,22 +1,33 @@ +from django.utils import timezone +from rest_framework import serializers +from datetime import timedelta from ara.classes.serializers import MetaDataModelSerializer from apps.core.models.communication_article import CommunicationArticle -class CommunicationArticleSerializer(MetaDataModelSerializer): +class BaseCommunicationArticleSerializer(MetaDataModelSerializer): class Meta: model = CommunicationArticle fields = '__all__' - # TODO: is_confirmed_by_school 등을 timestamp 말고 progress_statues enum으로 바꿔서 프론트 보내주기 - # 좋아요 30개 달설 전, 달성 후 확인 전, 확인 후 답변전, 답변 완료 -# class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): -# class Meta(BaseCommunicationArticleSerializer.Meta): +class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): + days_left = serializers.SerializerMethodField( + read_only=True, + ) + @staticmethod + def get_days_left(obj) -> int: + no_deadline = 30 + if obj.response_deadline == timezone.datetime.min.replace(tzinfo=timezone.utc): + return no_deadline + else: + return ((obj.response_deadline + timedelta(hours=9)).date() - timezone.localtime().now().date()).days -class CommunicationArticleUpdateActionSerializer(CommunicationArticleSerializer): - class Meta(CommunicationArticleSerializer.Meta): + +class CommunicationArticleUpdateActionSerializer(BaseCommunicationArticleSerializer): + class Meta(BaseCommunicationArticleSerializer.Meta): read_only_fields = ( 'article', 'response_deadline', diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index 6944b390..f816052e 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -7,14 +7,16 @@ from ara.classes.viewset import ActionAPIViewSet from apps.core.models.communication_article import CommunicationArticle -from apps.core.serializers.communication_article import CommunicationArticleSerializer, CommunicationArticleUpdateActionSerializer +from apps.core.serializers.communication_article import BaseCommunicationArticleSerializer, \ + CommunicationArticleUpdateActionSerializer, CommunicationArticleSerializer class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): queryset = CommunicationArticle.objects.all() - serializer_class = CommunicationArticleSerializer + serializer_class = BaseCommunicationArticleSerializer action_serializer_class = { 'update': CommunicationArticleUpdateActionSerializer, + 'list': CommunicationArticleSerializer, } permission_classes = ( permissions.IsAuthenticated, From 98b0ebdc556e0a123709c73173afd08cfe4eae61 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Fri, 29 Apr 2022 03:04:52 +0900 Subject: [PATCH 050/101] Add test cases for communication article admin API --- .../core/serializers/communication_article.py | 18 +-- .../views/viewsets/communication_article.py | 4 +- tests/test_articles.py | 2 - tests/test_communication_article.py | 115 +++++++++++++++--- 4 files changed, 113 insertions(+), 26 deletions(-) diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index 43bff13b..d8e7699a 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -7,16 +7,25 @@ class BaseCommunicationArticleSerializer(MetaDataModelSerializer): - positive_vote_count = serializers.IntegerField(source='article.positive_vote_count') class Meta: model = CommunicationArticle fields = '__all__' + read_only_fields = ( + 'article', + 'response_deadline', + 'answered_at', + 'school_response_status', + ) class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): days_left = serializers.SerializerMethodField( read_only=True, ) + positive_vote_count = serializers.IntegerField( + source='article.positive_vote_count', + read_only=True, + ) @staticmethod def get_days_left(obj) -> int: @@ -29,9 +38,4 @@ def get_days_left(obj) -> int: class CommunicationArticleUpdateActionSerializer(BaseCommunicationArticleSerializer): class Meta(BaseCommunicationArticleSerializer.Meta): - read_only_fields = ( - 'article', - 'response_deadline', - 'answered_at', - 'school_response_status' - ) + pass diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index d581338e..5a01174d 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -3,10 +3,11 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status, filters, viewsets, response, permissions +from apps.core.models import Article from apps.core.permissions.communication_article import CommunicationArticleAdminPermission from ara.classes.viewset import ActionAPIViewSet -from apps.core.models.communication_article import CommunicationArticle +from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus from apps.core.serializers.communication_article import BaseCommunicationArticleSerializer, \ CommunicationArticleUpdateActionSerializer, CommunicationArticleSerializer @@ -41,5 +42,6 @@ def update(self, request, *args, **kwargs): def perform_update(self, serializer): serializer.save( confirmed_by_school_at=timezone.now(), + school_response_status=SchoolResponseStatus.PREPARING_ANSWER, ) return super().perform_update(serializer) diff --git a/tests/test_articles.py b/tests/test_articles.py index f1724bcb..dc533063 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -2,8 +2,6 @@ from django.contrib.auth.models import User from django.utils import timezone -from django.conf import settings - from apps.core.models import Article, Topic, Board, Block, Vote, Comment from apps.core.models.board import BoardNameType from apps.user.models import UserProfile diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 20f96d57..a6fe118a 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -1,3 +1,5 @@ +from datetime import timedelta, datetime + import pytest from django.utils import timezone @@ -8,6 +10,7 @@ from apps.core.models import Article, Board from apps.core.models.board import BoardNameType from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus +from apps.core.serializers.communication_article import CommunicationArticleSerializer from apps.user.models import UserProfile from tests.conftest import RequestSetting, TestCase @@ -192,11 +195,11 @@ def test_non_communication_article(self): # 0 -> 1 def test_BEFORE_UPVOTE_THRESHOLD_to_BEFORE_SCHOOL_CONFIRM(self): - from_stauts = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD + from_status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD to_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM - article = self._create_article_with_status(from_stauts) - assert self._get_communication_article_status(article) == from_stauts + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status users_tuple = (self.user2, self.user3, self.user4) self._add_upvotes(article, users_tuple) @@ -204,8 +207,16 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_BEFORE_SCHOOL_CONFIRM(self): # 0 -> 2 def test_BEFORE_UPVOTE_THRESHOLD_to_PREPARING_ANSWER(self): - # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 - pass + from_status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD + to_status = SchoolResponseStatus.PREPARING_ANSWER + + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status + + self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}', { + 'confirmed_by_school_at': timezone.now(), + }) + assert self._get_communication_article_status(article) == to_status # 0 -> 3 def test_BEFORE_UPVOTE_THRESHOLD_to_ANSWER_DONE(self): @@ -220,8 +231,16 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_ANSWER_DONE(self): # 1 -> 2 def test_BEFORE_SCHOOL_CONFIRM_to_PREPARING_ANSWER(self): - # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 - pass + from_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + to_status = SchoolResponseStatus.PREPARING_ANSWER + + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status + + self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}', { + 'confirmed_by_school_at': timezone.now(), + }) + assert self._get_communication_article_status(article) == to_status # 1 -> 3 def test_BEFORE_SCHOOL_CONFIRM_to_ANSWER_DONE(self): @@ -244,12 +263,20 @@ def test_PREPARING_ANSWER_to_ANSWER_DONE(self): self._add_admin_comment(article) assert self._get_communication_article_status(article) == to_status - - # 2 -> 1 (관리자가 '답변 준비 중' 취소하는 경우) + + # 2 -> 1 (관리자가 답변 준비 중일 때 댓글 수가 다시 증가할 경우) def test_PREPARING_ANSWER_to_BEFORE_SCHOOL_CONFIRM(self): - # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 - pass - + from_status = SchoolResponseStatus.PREPARING_ANSWER + to_status = SchoolResponseStatus.PREPARING_ANSWER + + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status + + # 정책상 소통게시판에서 vote_cancel은 허용되지 않으나, 테스트 정확성을 위해 첨부함 + self.http_request(self.user4, 'post', f'articles/{article.id}/vote_cancel') + self.http_request(self.user4, 'post', f'articles/{article.id}/vote_positive') + assert self._get_communication_article_status(article) == to_status + # 3 -> 1 def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): status = SchoolResponseStatus.ANSWER_DONE @@ -263,9 +290,17 @@ def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): # 3 -> 2 def test_ANSWER_DONE_to_PREPARING_ANSWER(self): - # TODO: '답변 준비 중' 버튼 제작 후 마저 작성 - pass - + from_status = SchoolResponseStatus.ANSWER_DONE + to_status = SchoolResponseStatus.ANSWER_DONE + + article = self._create_article_with_status(from_status) + assert self._get_communication_article_status(article) == from_status + + # 정책상 소통게시판에서 vote_cancel은 허용되지 않으나, 테스트 정확성을 위해 첨부함 + self.http_request(self.user4, 'post', f'articles/{article.id}/vote_cancel') + self.http_request(self.user4, 'post', f'articles/{article.id}/vote_positive') + assert self._get_communication_article_status(article) == to_status + # ======================================================================= # # Ordering & Filtering # # ======================================================================= # @@ -309,5 +344,53 @@ def test_anonymous_comment(self): res = self.http_request(self.user, 'post', 'comments', comment_data).data assert res.get('name_type') == BoardNameType.REALNAME - + # ======================================================================= # + # Permission # + # ======================================================================= # + def test_admin_can_view_admin_api_list(self): + res = self.http_request(self.school_admin, 'get', 'communication_articles') + assert res.status_code == 200 + assert res.data.get('num_items') == CommunicationArticle.objects.all().count() + + def test_admin_can_view_admin_api_get(self): + res = self.http_request(self.school_admin, 'get', f'communication_articles/{self.article.id}') + self.communication_article.refresh_from_db() + assert res.status_code == 200 + assert res.data.get('article') == self.communication_article.article.id + assert res.data.get('school_response_status') == self.communication_article.school_response_status + + def test_admin_can_update_admin_api(self): + current_time = timezone.now() + res = self.http_request(self.school_admin, 'put', f'communication_articles/{self.article.id}', { + 'confirmed_by_school_at': current_time, + }) + assert res.status_code == 200 + self.communication_article.refresh_from_db() + cas = CommunicationArticleSerializer(self.communication_article) + assert res.data.get('confirmed_by_school_at') == cas.data['confirmed_by_school_at'] + + def test_user_cannot_view_admin_api_list(self): + res = self.http_request(self.user, 'get', 'communication_articles') + assert res.status_code == 403 + + def test_user_cannot_view_admin_api_get(self): + res = self.http_request(self.user, 'get', f'communication_articles/{self.article.id}') + assert res.status_code == 403 + + def test_user_cannot_update_admin_api(self): + res = self.http_request(self.user, 'put', f'communication_articles/{self.article.id}', { + 'confirmed_by_school_at': timezone.now(), + }) + assert res.status_code == 403 + + def test_days_left(self): + days_left = 6 + current_time = timezone.now() + deadline = current_time + timedelta(days=days_left) + self.http_request(self.school_admin, 'put', f'communication_articles/{self.article.id}', { + 'confirmed_by_school_at': current_time, + 'response_deadline': deadline, + }) + res = self.http_request(self.school_admin, 'get', f'communication_articles/{self.article.id}') + return res.data.get('days_left') == days_left From 38cc017a364bb420290b5dae3ddd09ec3db5c6c7 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 2 May 2022 02:56:14 +0900 Subject: [PATCH 051/101] Remove duplicated created_by in CommentSerializer --- apps/core/serializers/comment.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/core/serializers/comment.py b/apps/core/serializers/comment.py index 9e2b74dc..a3b3268b 100644 --- a/apps/core/serializers/comment.py +++ b/apps/core/serializers/comment.py @@ -44,9 +44,6 @@ def get_created_by(self, obj) -> dict: class CommentSerializer(HiddenSerializerFieldMixin, BaseCommentSerializer): from apps.user.serializers.user import PublicUserSerializer - created_by = PublicUserSerializer( - read_only=True, - ) my_vote = serializers.SerializerMethodField( read_only=True, ) @@ -63,9 +60,6 @@ class CommentSerializer(HiddenSerializerFieldMixin, BaseCommentSerializer): class CommentListActionSerializer(HiddenSerializerFieldMixin, BaseCommentSerializer): from apps.user.serializers.user import PublicUserSerializer - created_by = PublicUserSerializer( - read_only=True, - ) my_vote = serializers.SerializerMethodField( read_only=True, ) From 88a8ac6bd26b06f03c20fc3397f306f42920d9e6 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 2 May 2022 04:21:11 +0900 Subject: [PATCH 052/101] Add communication_article_status filter --- apps/core/filters/article.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/core/filters/article.py b/apps/core/filters/article.py index c8b9123a..985bc782 100644 --- a/apps/core/filters/article.py +++ b/apps/core/filters/article.py @@ -47,6 +47,10 @@ class Meta: 'in', 'exact', ], + 'communication_article__school_response_status': [ + 'exact', + 'lt' + ] } main_search__contains = filters.CharFilter( From 7973a32ff07ca9e33cc1a94e0ed182776b51b048 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 2 May 2022 04:21:34 +0900 Subject: [PATCH 053/101] Remove communication_article viewset --- .../views/viewsets/communication_article.py | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 apps/core/views/viewsets/communication_article.py diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py deleted file mode 100644 index 6898e054..00000000 --- a/apps/core/views/viewsets/communication_article.py +++ /dev/null @@ -1,20 +0,0 @@ -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, viewsets - -from ara.classes.viewset import ActionAPIViewSet - -from apps.core.models.communication_article import CommunicationArticle -from apps.core.serializers.communication_article import CommunicationArticleSerializer - - -class CommunicationArticleViewSet(viewsets.ReadOnlyModelViewSet, ActionAPIViewSet): - queryset = CommunicationArticle.objects.all() - serializer_class = CommunicationArticleSerializer - filter_backends = [filters.OrderingFilter, DjangoFilterBackend] - - # usage: /api/communication_articles/?ordering=created_at - ordering_fields = ['article__positive_vote_count'] - ordering = ['-article__positive_vote_count'] # default: 추천수 내림차순 - - # usage: /api/communication_articles/?school_response_status=1 - filterset_fields = ['school_response_status'] From 96e229bcdc46eb94827690962b1964c134e8df75 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 2 May 2022 05:03:52 +0900 Subject: [PATCH 054/101] Update router & init files --- apps/core/views/router.py | 6 ------ apps/core/views/viewsets/__init__.py | 1 - 2 files changed, 7 deletions(-) diff --git a/apps/core/views/router.py b/apps/core/views/router.py index 13f27c65..1ec37c6e 100644 --- a/apps/core/views/router.py +++ b/apps/core/views/router.py @@ -63,9 +63,3 @@ prefix=r'best_searches', viewset=viewsets.BestSearchViewSet, ) - -# CommunicationArticleViewSet -router.register( - prefix=r'communication_articles', - viewset=viewsets.CommunicationArticleViewSet, -) diff --git a/apps/core/views/viewsets/__init__.py b/apps/core/views/viewsets/__init__.py index a280de75..89e9160d 100644 --- a/apps/core/views/viewsets/__init__.py +++ b/apps/core/views/viewsets/__init__.py @@ -8,4 +8,3 @@ from .scrap import * from .faq import * from .best_search import * -from .communication_article import * From df6d21fe945d9c3644d8891bd11ed6037cc80039 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Mon, 2 May 2022 15:35:10 +0900 Subject: [PATCH 055/101] change field name get_realname to realname --- apps/core/models/article.py | 2 +- apps/core/models/comment.py | 2 +- apps/user/models/user_profile.py | 2 +- tests/test_articles.py | 8 ++++---- tests/test_comments.py | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 0ca21753..1b35dc4c 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -229,7 +229,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: } if self.name_type == BoardNameType.REALNAME: - user_realname = self.created_by.profile.get_realname + user_realname = self.created_by.profile.realname return { 'id': user_unique_num, diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index a1a35a11..2d4bd71f 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -192,7 +192,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: if parent_article_created_by_id == comment_created_by_id: user_realname = gettext('author') else: - user_realname = self.created_by.profile.get_realname + user_realname = self.created_by.profile.realname return { 'id': user_unique_num, diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index 287c99cd..f07f68fa 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -135,7 +135,7 @@ def email(self) -> str: return self.user.email @cached_property - def get_realname(self) -> str: + def realname(self) -> str: sso_info = self.sso_user_info user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] diff --git a/tests/test_articles.py b/tests/test_articles.py index e12c7761..f4bbe674 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -235,19 +235,19 @@ def test_realname_article(self): # 익명 게시글을 GET할 때, 작성자의 정보가 실명으로 전달되는 것 확인 res = self.http_request(self.user, 'get', f'articles/{realname_article.id}').data assert res.get('name_type') == BoardNameType.REALNAME - assert res.get('created_by')['username'] == realname_article.created_by.profile.get_realname + assert res.get('created_by')['username'] == realname_article.created_by.profile.realname res2 = self.http_request(self.user2, 'get', f'articles/{realname_article.id}').data assert res2.get('name_type') == BoardNameType.REALNAME - assert res2.get('created_by')['username'] == realname_article.created_by.profile.get_realname + assert res2.get('created_by')['username'] == realname_article.created_by.profile.realname res3 = self.http_request(self.user3, 'get', f'articles/{realname_article.id}').data assert res3.get('name_type') == BoardNameType.REALNAME - assert res3.get('created_by')['username'] == realname_article.created_by.profile.get_realname + assert res3.get('created_by')['username'] == realname_article.created_by.profile.realname res4 = self.http_request(self.user4, 'get', f'articles/{realname_article.id}').data assert res4.get('name_type') == BoardNameType.REALNAME - assert res4.get('created_by')['username'] == realname_article.created_by.profile.get_realname + assert res4.get('created_by')['username'] == realname_article.created_by.profile.realname def test_create(self): # test_create: HTTP request (POST)를 이용해서 생성 diff --git a/tests/test_comments.py b/tests/test_comments.py index 5e18d12f..380fa61a 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -305,19 +305,19 @@ def test_realname_comment(self): # 익명 댓글을 GET할 때, 작성자의 정보가 전달되지 않는 것 확인 res = self.http_request(self.user, 'get', f'comments/{realname_comment.id}').data assert res.get('name_type') == BoardNameType.REALNAME - assert res.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + assert res.get('created_by')['username'] != realname_comment.created_by.profile.realname res2 = self.http_request(self.user2, 'get', f'comments/{realname_comment.id}').data assert res2.get('name_type') == BoardNameType.REALNAME - assert res2.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + assert res2.get('created_by')['username'] != realname_comment.created_by.profile.realname res3 = self.http_request(self.user3, 'get', f'comments/{realname_comment.id}').data assert res3.get('name_type') == BoardNameType.REALNAME - assert res3.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + assert res3.get('created_by')['username'] != realname_comment.created_by.profile.realname res4 = self.http_request(self.user4, 'get', f'comments/{realname_comment.id}').data assert res4.get('name_type') == BoardNameType.REALNAME - assert res4.get('created_by')['username'] != realname_comment.created_by.profile.get_realname + assert res4.get('created_by')['username'] != realname_comment.created_by.profile.realname def test_realname_comment_by_article_writer(self): # 익명 댓글 생성 From 7eee9dfc65b9ab57d71de0c904835b622fa026ab Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 3 May 2022 00:22:52 +0900 Subject: [PATCH 056/101] Add positive_vote_count ordering --- apps/core/views/viewsets/article.py | 3 +++ ara/settings/djangorestframework.py | 1 + 2 files changed, 4 insertions(+) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index 8c14090c..c658349e 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -34,7 +34,10 @@ class ArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): queryset = Article.objects.all() + filterset_class = ArticleFilter + ordering_fields = ['positive_vote_count'] + serializer_class = ArticleSerializer action_serializer_class = { 'list': ArticleListActionSerializer, diff --git a/ara/settings/djangorestframework.py b/ara/settings/djangorestframework.py index c1038c53..bb92c239 100644 --- a/ara/settings/djangorestframework.py +++ b/ara/settings/djangorestframework.py @@ -4,6 +4,7 @@ 'DEFAULT_PAGINATION_CLASS': 'ara.classes.pagination.PageNumberPagination', 'DEFAULT_FILTER_BACKENDS': ( 'rest_framework_filters.backends.RestFrameworkFilterBackend', + 'rest_framework.filters.OrderingFilter', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.AllowAny', From fd95afffef03936741caf330d344ba2f28f26923 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 5 May 2022 21:38:46 +0900 Subject: [PATCH 057/101] Add filtering & ordering tests --- tests/test_communication_article.py | 118 ++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 17 deletions(-) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index af9f0201..c7f00b85 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -1,9 +1,13 @@ import pytest +from datetime import datetime from django.utils import timezone from django.contrib.auth.models import User from rest_framework.test import APIClient +from rest_framework.status import HTTP_200_OK + +from ara.settings.dev import SCHOOL_RESPONSE_VOTE_THRESHOLD from apps.core.models import Article, Board from apps.core.models.board import BoardNameType @@ -92,9 +96,32 @@ class TestCommunicationArticle(TestCase, RequestSetting): def _get_communication_article_status(self, article): res = self.http_request(self.user, 'get', f'articles/{article.id}').data return res.get('communication_article_status') - - def _add_upvotes(self, article, users): - for user in users: + + def _create_dummy_users(self, num): + dummy_users = [] + for i in range(num): + user, created = User.objects.get_or_create( + username=f'DummyUser{i}', + email=f'dummy_user{i}@sparcs.org' + ) + if created: + UserProfile.objects.create( + user=user, + nickname=f'User{i} created at {timezone.now()}', + group=UserProfile.UserGroup.KAIST_MEMBER, + agree_terms_of_service_at=timezone.now(), + sso_user_info={ + 'kaist_info': '{\"ku_kname\": \"\\ud669\"}', + 'first_name': f'DummyUser{i}_FirstName', + 'last_name': f'DummyUser{i}_LastName' + } + ) + dummy_users.append(user) + return dummy_users + + def _add_positive_votes(self, article, num): + dummy_users = self._create_dummy_users(num) + for user in dummy_users: self.http_request(user, 'post', f'articles/{article.id}/vote_positive') def _add_admin_comment(self, article): @@ -107,7 +134,7 @@ def _add_admin_comment(self, article): # status를 가지는 communication_article 반환 def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD): - article_title = f'Factory: Article Status {status}' + article_title = f'Factory: Article Status {status} created at {timezone.now()}' article_data = { 'title': article_title, 'content': 'Content made in factory', @@ -123,8 +150,7 @@ def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_ if status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: pass elif status == SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM: - users_tuple = (self.user2, self.user3, self.user4) - self._add_upvotes(article, users_tuple) + self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) elif status == SchoolResponseStatus.PREPARING_ANSWER: # 작성일 기준 status 변경 방법이 존재하지 않음 article.communication_article.response_deadline = timezone.now() @@ -198,8 +224,7 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_BEFORE_SCHOOL_CONFIRM(self): article = self._create_article_with_status(from_stauts) assert self._get_communication_article_status(article) == from_stauts - users_tuple = (self.user2, self.user3, self.user4) - self._add_upvotes(article, users_tuple) + self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) assert self._get_communication_article_status(article) == to_status # 0 -> 2 @@ -257,8 +282,7 @@ def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): article = self._create_article_with_status(status) assert self._get_communication_article_status(article) == status - users_tuple = (self.user2, self.user3, self.user4) - self._add_upvotes(article, users_tuple) + self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) assert self._get_communication_article_status(article) == status # 3 -> 2 @@ -270,16 +294,76 @@ def test_ANSWER_DONE_to_PREPARING_ANSWER(self): # Ordering & Filtering # # ======================================================================= # - # 좋아요 개수로 정렬 확인 - def test_ordering_by_positive_vote_count(self): - # TODO: In different branch - pass + # 좋아요 개수 내림차순 정렬 확인 + def test_descending_ordering_by_positive_vote_count(self): + vote_counts = (0, 1, 2, 2, 3, 4) + articles = [self._create_article_with_status() for _ in vote_counts] + + for vote_cnt, article in zip(vote_counts, articles): + self._add_positive_votes(article, vote_cnt) + article.refresh_from_db() + + res = self.http_request(self.user, 'get', 'articles', + querystring='ordering=-positive_vote_count') + assert res.status_code == HTTP_200_OK + res_result = res.data.get('results') + + # 좋아요 개수 내림차순 정렬 확인 + res_positive_votes = [el.get('positive_vote_count') for el in res_result] + assert res_positive_votes == sorted(res_positive_votes, reverse=True) + + # 좋아요 개수 같은 경우 최신 글이 앞에 있는지 확인 + res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] + print(res_vote_cnt_eq) + assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) + assert False + + # 좋아요 개수 오름차순 정렬 확인 + def test_ascending_ordering_by_positive_vote_count(self): + vote_counts = (4, 3, 2, 2, 1, 0) + articles = [self._create_article_with_status() for _ in vote_counts] + + for vote_cnt, article in zip(vote_counts, articles): + self._add_positive_votes(article, vote_cnt) + article.refresh_from_db() + + res = self.http_request(self.user, 'get', 'articles', + querystring='ordering=positive_vote_count') + assert res.status_code == HTTP_200_OK + + res_result = res.data.get('results') + + # 좋아요 개수 오름차순 정렬 확인 + res_positive_votes = [el.get('positive_vote_count') for el in res_result] + assert res_positive_votes == sorted(res_positive_votes) + + # 좋아요 개수 같은 경우 최신 글이 앞에 있는지 확인 + res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] + print(res_vote_cnt_eq) + assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) + assert False + # 답변 진행 상황 필터링 확인 def test_filtering_by_status(self): - # TODO: In different branch - pass - + # 모든 status의 article 하나씩 생성 + for status in SchoolResponseStatus: + self._create_article_with_status(status) + + # 답변 완료(status=3) + res = self.http_request(self.user, 'get', 'articles', + querystring='communication_article__school_response_status=3') + assert res.status_code == HTTP_200_OK + res_status_set = {el.get('communication_article_status') for el in res.data.get('results')} + assert res_status_set == {SchoolResponseStatus.ANSWER_DONE} + + # 답변 전(status<3) + res = self.http_request(self.user, 'get', 'articles', + querystring='communication_article__school_response_status__lt=3') + assert res.status_code == HTTP_200_OK + res_status_set = {el.get('communication_article_status') for el in res.data.get('results')} + assert res_status_set == {*SchoolResponseStatus} - {SchoolResponseStatus.ANSWER_DONE} + # ======================================================================= # # Anonymous # # ======================================================================= # From 3efe502a1938cdaa35352e0ed69e4d370097867f Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 5 May 2022 22:54:02 +0900 Subject: [PATCH 058/101] Update ordering tests for PR, temporarily --- tests/test_communication_article.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index c7f00b85..e9a64dd9 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -314,10 +314,10 @@ def test_descending_ordering_by_positive_vote_count(self): assert res_positive_votes == sorted(res_positive_votes, reverse=True) # 좋아요 개수 같은 경우 최신 글이 앞에 있는지 확인 - res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] - print(res_vote_cnt_eq) - assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) - assert False + # res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] + # print(res_vote_cnt_eq) + # assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) + # assert False # 좋아요 개수 오름차순 정렬 확인 def test_ascending_ordering_by_positive_vote_count(self): @@ -339,10 +339,10 @@ def test_ascending_ordering_by_positive_vote_count(self): assert res_positive_votes == sorted(res_positive_votes) # 좋아요 개수 같은 경우 최신 글이 앞에 있는지 확인 - res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] - print(res_vote_cnt_eq) - assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) - assert False + # res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] + # print(res_vote_cnt_eq) + # assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) + # assert False # 답변 진행 상황 필터링 확인 def test_filtering_by_status(self): From ffedf69aa0693c1de37bb6d319aff453af3832cf Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Fri, 6 May 2022 00:09:18 +0900 Subject: [PATCH 059/101] Fix ordering when votes counts are equal --- apps/core/views/viewsets/article.py | 2 +- tests/test_communication_article.py | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index c658349e..30328788 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -36,7 +36,7 @@ class ArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): queryset = Article.objects.all() filterset_class = ArticleFilter - ordering_fields = ['positive_vote_count'] + ordering_fields = ['created_at', 'positive_vote_count'] serializer_class = ArticleSerializer action_serializer_class = { diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index e9a64dd9..a4ff73f0 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -304,7 +304,7 @@ def test_descending_ordering_by_positive_vote_count(self): article.refresh_from_db() res = self.http_request(self.user, 'get', 'articles', - querystring='ordering=-positive_vote_count') + querystring='ordering=-positive_vote_count,-created_at') assert res.status_code == HTTP_200_OK res_result = res.data.get('results') @@ -314,10 +314,8 @@ def test_descending_ordering_by_positive_vote_count(self): assert res_positive_votes == sorted(res_positive_votes, reverse=True) # 좋아요 개수 같은 경우 최신 글이 앞에 있는지 확인 - # res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] - # print(res_vote_cnt_eq) - # assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) - # assert False + res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] + assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) # 좋아요 개수 오름차순 정렬 확인 def test_ascending_ordering_by_positive_vote_count(self): @@ -329,7 +327,7 @@ def test_ascending_ordering_by_positive_vote_count(self): article.refresh_from_db() res = self.http_request(self.user, 'get', 'articles', - querystring='ordering=positive_vote_count') + querystring='ordering=positive_vote_count,-created_at') assert res.status_code == HTTP_200_OK res_result = res.data.get('results') @@ -339,10 +337,8 @@ def test_ascending_ordering_by_positive_vote_count(self): assert res_positive_votes == sorted(res_positive_votes) # 좋아요 개수 같은 경우 최신 글이 앞에 있는지 확인 - # res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] - # print(res_vote_cnt_eq) - # assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) - # assert False + res_vote_cnt_eq = [el.get('created_at') for el in res_result if el.get('positive_vote_count') == 2] + assert res_vote_cnt_eq == sorted(res_vote_cnt_eq, reverse=True, key=lambda date_str: datetime.fromisoformat(date_str)) # 답변 진행 상황 필터링 확인 def test_filtering_by_status(self): From 8e4901b2e0b1bbd135346ca7882c9dbd6204b1a3 Mon Sep 17 00:00:00 2001 From: kjy2844 Date: Fri, 6 May 2022 00:27:23 +0900 Subject: [PATCH 060/101] Fix PR review --- apps/core/models/article.py | 28 +++--- apps/core/models/board.py | 4 +- apps/core/serializers/article.py | 19 ++-- .../core/serializers/communication_article.py | 12 +-- apps/core/views/viewsets/article.py | 5 +- .../views/viewsets/communication_article.py | 11 +-- ara/settings/dev/__init__.py | 1 + tests/test_communication_article.py | 98 ++++++++----------- 8 files changed, 82 insertions(+), 96 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index fa3a735f..0940e5c9 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -16,7 +16,7 @@ from ara.classes.decorator import cache_by_user from ara.db.models import MetaDataModel from ara.sanitizer import sanitize -from ara.settings import HASH_SECRET_VALUE, SCHOOL_RESPONSE_VOTE_THRESHOLD +from ara.settings import HASH_SECRET_VALUE, SCHOOL_RESPONSE_VOTE_THRESHOLD, ANSWER_PERIOD from .block import Block from .report import Report from .comment import Comment @@ -72,7 +72,7 @@ class Meta(MetaDataModel.Meta): ) report_count = models.IntegerField( default=0, - verbose_name ='신고 수', + verbose_name='신고 수', ) positive_vote_count = models.IntegerField( default=0, @@ -171,25 +171,27 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) def update_hit_count(self): - self.hit_count = self.article_read_log_set.values('read_by').annotate(models.Count('read_by')).order_by('read_by__count',).count() + self.migrated_hit_count + self.hit_count = self.article_read_log_set.values('read_by').annotate(models.Count('read_by')).order_by( + 'read_by__count', ).count() + self.migrated_hit_count self.save() def update_comment_count(self): - self.comment_count = Comment.objects.filter(deleted_at=timezone.datetime.min.replace(tzinfo=timezone.utc)).filter( + self.comment_count = Comment.objects.filter( + deleted_at=timezone.datetime.min.replace(tzinfo=timezone.utc)).filter( models.Q(parent_article=self) | models.Q(parent_comment__parent_article=self) ).count() self.save() - + @transaction.atomic def update_report_count(self): count = Report.objects.filter(parent_article=self).count() self.report_count = count if count >= int(settings.REPORT_THRESHOLD): - self.hidden_at = timezone.now() + self.hidden_at = timezone.now() self.save() @@ -197,10 +199,11 @@ def update_vote_status(self): self.positive_vote_count = self.vote_set.filter(is_positive=True).count() + self.migrated_positive_vote_count self.negative_vote_count = self.vote_set.filter(is_positive=False).count() + self.migrated_negative_vote_count - if self.parent_board.is_school_communication and self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: - self.communication_article.response_deadline = timezone.now() + timezone.timedelta(days=14) - if self.communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: + if self.parent_board.is_school_communication: + if self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD and self.communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: self.communication_article.school_response_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + self.communication_article.response_deadline = ( + timezone.localtime() + timezone.timedelta(days=ANSWER_PERIOD + 1)).date() self.communication_article.save() self.save() @@ -217,7 +220,7 @@ def created_by_nickname(self): def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: if self.name_type == BoardNameType.REGULAR: return self.created_by - + user_unique_num = self.created_by.id + self.id + HASH_SECRET_VALUE user_unique_encoding = str(hex(user_unique_num)).encode('utf-8') user_hash = hashlib.sha224(user_unique_encoding).hexdigest() @@ -234,10 +237,11 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'user': gettext('anonymous') }, } - + if self.name_type == BoardNameType.REALNAME: sso_info = self.created_by.profile.sso_user_info - user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] + user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] \ + if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] return { 'id': user_unique_num, diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 42d6c3e7..51258a0d 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -4,11 +4,13 @@ from ara.db.models import MetaDataModel + class BoardNameType(IntEnum): REGULAR = 0 ANONYMOUS = 1 REALNAME = 2 + class Board(MetaDataModel): class Meta(MetaDataModel.Meta): verbose_name = '게시판' @@ -42,7 +44,7 @@ class Meta(MetaDataModel.Meta): # access_mask & (1< 0 일 때 접근이 가능합니다. # 사용자 그룹의 값들은 `UserGroup`을 참고하세요. access_mask = models.IntegerField( - default=int('11011111', 2), # 카이스트 구성원만 사용 가능 + default=int('11011110', 2), # 카이스트 구성원만 사용 가능 null=False, verbose_name='접근 권한 값' ) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 2970c555..1ef4350b 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -9,7 +9,6 @@ from apps.core.models import Article, Board, Block, Scrap, ArticleHiddenReason, Comment from apps.core.models.board import BoardNameType from apps.core.serializers.board import BoardSerializer -from apps.core.serializers.communication_article import BaseCommunicationArticleSerializer from apps.core.serializers.mixins.hidden import HiddenSerializerMixin, HiddenSerializerFieldMixin from apps.core.serializers.topic import TopicSerializer from apps.user.serializers.user import PublicUserSerializer @@ -119,7 +118,8 @@ class Meta(BaseArticleSerializer.Meta): class ArticleSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): class Meta(BaseArticleSerializer.Meta): - exclude = ('migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) + exclude = ( + 'migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) @staticmethod def search_articles(queryset, search): @@ -200,7 +200,7 @@ def get_side_articles(self, obj) -> dict: after = after_scrap.parent_article else: after = None - + else: before = articles.filter(created_at__lte=obj.created_at).first() after = articles.filter(created_at__gte=obj.created_at).last() @@ -269,13 +269,13 @@ def get_side_articles_of_recent_article(self, obj, request): after = None if len(after) == 0 else after[0] return after, before - def get_attachments(self, obj): # -> typing.Optional[list]: + def get_attachments(self, obj): # -> typing.Optional[list]: if self.visible_verdict(obj): return obj.attachments.all().values_list('id') return None - def get_my_comment_profile(self, obj): - fake_comment = Comment(created_by = self.context['request'].user, name_type = obj.name_type, parent_article = obj) + def get_my_comment_profile(self, obj): + fake_comment = Comment(created_by=self.context['request'].user, name_type=obj.name_type, parent_article=obj) if obj.name_type in (BoardNameType.ANONYMOUS, BoardNameType.REALNAME): return fake_comment.postprocessed_created_by else: @@ -338,6 +338,7 @@ def get_communication_article_status(obj): return obj.communication_article.school_response_status return None + class ArticleAttachmentType(Enum): NONE = 'NONE' IMAGE = 'IMAGE' @@ -407,7 +408,8 @@ class BestArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSer class ArticleCreateActionSerializer(BaseArticleSerializer): class Meta(BaseArticleSerializer.Meta): - exclude = ('migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) + exclude = ( + 'migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) read_only_fields = ( 'hit_count', 'comment_count', @@ -429,7 +431,8 @@ def validate_parent_board(self, board: Board): class ArticleUpdateActionSerializer(BaseArticleSerializer): class Meta(BaseArticleSerializer.Meta): - exclude = ('migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) + exclude = ( + 'migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) read_only_fields = ( 'name_type', 'hit_count', diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index d8e7699a..46da9ee3 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -1,5 +1,6 @@ +import sys + from django.utils import timezone -from datetime import timedelta from rest_framework import serializers from ara.classes.serializers import MetaDataModelSerializer @@ -22,18 +23,13 @@ class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): days_left = serializers.SerializerMethodField( read_only=True, ) - positive_vote_count = serializers.IntegerField( - source='article.positive_vote_count', - read_only=True, - ) @staticmethod def get_days_left(obj) -> int: - no_deadline = 30 if obj.response_deadline == timezone.datetime.min.replace(tzinfo=timezone.utc): - return no_deadline + return sys.maxsize else: - return ((obj.response_deadline + timedelta(hours=9)).date() - timezone.localtime().now().date()).days + return (obj.response_deadline.astimezone(timezone.localtime().tzinfo) - timezone.localtime()).days class CommunicationArticleUpdateActionSerializer(BaseCommunicationArticleSerializer): diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index 8c14090c..26159a2c 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -309,7 +309,8 @@ def recent(self, request, *args, **kwargs): self.paginate_queryset(count_queryset) - query_params = [self.request.user.id, self.paginator.get_page_size(request), max(0, self.paginator.page.start_index()-1)] + query_params = [self.request.user.id, self.paginator.get_page_size(request), + max(0, self.paginator.page.start_index() - 1)] if search_keyword: query_params.insert(1, id_set) @@ -325,7 +326,7 @@ def recent(self, request, *args, **kwargs): LIMIT %s OFFSET %s ) recents ON recents.article_id = `core_article`.id ORDER BY recents.my_last_read_at desc - ''', + ''', query_params ).prefetch_related( 'created_by', diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index 5a01174d..9b0d2718 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -1,20 +1,17 @@ -from django.utils.translation import gettext from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status, filters, viewsets, response, permissions -from apps.core.models import Article from apps.core.permissions.communication_article import CommunicationArticleAdminPermission from ara.classes.viewset import ActionAPIViewSet from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus -from apps.core.serializers.communication_article import BaseCommunicationArticleSerializer, \ - CommunicationArticleUpdateActionSerializer, CommunicationArticleSerializer +from apps.core.serializers.communication_article import CommunicationArticleUpdateActionSerializer, CommunicationArticleSerializer class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): queryset = CommunicationArticle.objects.all() - serializer_class = BaseCommunicationArticleSerializer + serializer_class = CommunicationArticleSerializer action_serializer_class = { 'update': CommunicationArticleUpdateActionSerializer, 'list': CommunicationArticleSerializer, @@ -34,8 +31,8 @@ class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): # 학교 담당자가 신문고 게시글에 대해 `확인했습니다` 버튼을 누른 경우 def update(self, request, *args, **kwargs): - # user가 학교 담당자인지 확인 - if self.get_object().confirmed_by_school_at != timezone.datetime.min.replace(tzinfo=timezone.utc): + # 이미 확인했습니다 단계를 지나간 경우 + if self.get_object().school_response_status > SchoolResponseStatus.PREPARING_ANSWER: return response.Response(status=status.HTTP_200_OK) return super().update(request, *args, **kwargs) diff --git a/ara/settings/dev/__init__.py b/ara/settings/dev/__init__.py index 759e8a22..765d84d2 100644 --- a/ara/settings/dev/__init__.py +++ b/ara/settings/dev/__init__.py @@ -31,6 +31,7 @@ REPORT_THRESHOLD = 4 SCHOOL_RESPONSE_VOTE_THRESHOLD = 3 +ANSWER_PERIOD = 14 try: from .local_settings import * diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index a6fe118a..324404bd 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -1,10 +1,9 @@ -from datetime import timedelta, datetime - import pytest from django.utils import timezone from django.contrib.auth.models import User +from rest_framework import status from rest_framework.test import APIClient from apps.core.models import Article, Board @@ -12,6 +11,7 @@ from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus from apps.core.serializers.communication_article import CommunicationArticleSerializer from apps.user.models import UserProfile +from ara.settings import ANSWER_PERIOD from tests.conftest import RequestSetting, TestCase @@ -99,7 +99,10 @@ def _get_communication_article_status(self, article): def _add_upvotes(self, article, users): for user in users: self.http_request(user, 'post', f'articles/{article.id}/vote_positive') - + + def _confirm_communication_article(self, article): + self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}') + def _add_admin_comment(self, article): comment_data = { 'content': 'Comment made in factory', @@ -130,13 +133,12 @@ def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_ self._add_upvotes(article, users_tuple) elif status == SchoolResponseStatus.PREPARING_ANSWER: # 작성일 기준 status 변경 방법이 존재하지 않음 - article.communication_article.response_deadline = timezone.now() - article.communication_article.confirmed_by_school_at = timezone.now() - article.communication_article.school_response_status = SchoolResponseStatus.PREPARING_ANSWER - article.communication_article.save() + users_tuple = (self.user2, self.user3, self.user4) + self._add_upvotes(article, users_tuple) + self._confirm_communication_article(article) elif status == SchoolResponseStatus.ANSWER_DONE: self._add_admin_comment(article) - + return article # ======================================================================= # @@ -171,7 +173,7 @@ def test_create_communication_article(self): communication_article.answered_at == min_time, communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD ]) - + # 소통 게시물이 아닌 게시물에 communication_article 없는지 확인 def test_non_communication_article(self): # 비소통 게시물 생성 @@ -204,7 +206,7 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_BEFORE_SCHOOL_CONFIRM(self): users_tuple = (self.user2, self.user3, self.user4) self._add_upvotes(article, users_tuple) assert self._get_communication_article_status(article) == to_status - + # 0 -> 2 def test_BEFORE_UPVOTE_THRESHOLD_to_PREPARING_ANSWER(self): from_status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD @@ -213,9 +215,7 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_PREPARING_ANSWER(self): article = self._create_article_with_status(from_status) assert self._get_communication_article_status(article) == from_status - self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}', { - 'confirmed_by_school_at': timezone.now(), - }) + self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}') assert self._get_communication_article_status(article) == to_status # 0 -> 3 @@ -228,7 +228,7 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_ANSWER_DONE(self): self._add_admin_comment(article) assert self._get_communication_article_status(article) == to_status - + # 1 -> 2 def test_BEFORE_SCHOOL_CONFIRM_to_PREPARING_ANSWER(self): from_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM @@ -237,9 +237,7 @@ def test_BEFORE_SCHOOL_CONFIRM_to_PREPARING_ANSWER(self): article = self._create_article_with_status(from_status) assert self._get_communication_article_status(article) == from_status - self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}', { - 'confirmed_by_school_at': timezone.now(), - }) + self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}') assert self._get_communication_article_status(article) == to_status # 1 -> 3 @@ -264,18 +262,17 @@ def test_PREPARING_ANSWER_to_ANSWER_DONE(self): self._add_admin_comment(article) assert self._get_communication_article_status(article) == to_status - # 2 -> 1 (관리자가 답변 준비 중일 때 댓글 수가 다시 증가할 경우) + # 2 -> 1 (관리자가 답변 준비 중일 때 좋아요 수가 다시 증가할 경우) def test_PREPARING_ANSWER_to_BEFORE_SCHOOL_CONFIRM(self): - from_status = SchoolResponseStatus.PREPARING_ANSWER - to_status = SchoolResponseStatus.PREPARING_ANSWER + status = SchoolResponseStatus.PREPARING_ANSWER - article = self._create_article_with_status(from_status) - assert self._get_communication_article_status(article) == from_status + article = self._create_article_with_status(status) + assert self._get_communication_article_status(article) == status # 정책상 소통게시판에서 vote_cancel은 허용되지 않으나, 테스트 정확성을 위해 첨부함 self.http_request(self.user4, 'post', f'articles/{article.id}/vote_cancel') self.http_request(self.user4, 'post', f'articles/{article.id}/vote_positive') - assert self._get_communication_article_status(article) == to_status + assert self._get_communication_article_status(article) == status # 3 -> 1 def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): @@ -287,24 +284,28 @@ def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): users_tuple = (self.user2, self.user3, self.user4) self._add_upvotes(article, users_tuple) assert self._get_communication_article_status(article) == status - - # 3 -> 2 + + # 3 -> 2 (관리자가 답변 완료한 글에 다시 확인했습니다 요청을 보낸 경우) def test_ANSWER_DONE_to_PREPARING_ANSWER(self): - from_status = SchoolResponseStatus.ANSWER_DONE - to_status = SchoolResponseStatus.ANSWER_DONE + status = SchoolResponseStatus.ANSWER_DONE - article = self._create_article_with_status(from_status) - assert self._get_communication_article_status(article) == from_status + article = self._create_article_with_status(status) + assert self._get_communication_article_status(article) == status - # 정책상 소통게시판에서 vote_cancel은 허용되지 않으나, 테스트 정확성을 위해 첨부함 - self.http_request(self.user4, 'post', f'articles/{article.id}/vote_cancel') - self.http_request(self.user4, 'post', f'articles/{article.id}/vote_positive') - assert self._get_communication_article_status(article) == to_status + self._confirm_communication_article(article) + assert self._get_communication_article_status(article) == status + + def test_days_left(self): + status = SchoolResponseStatus.PREPARING_ANSWER + article = self._create_article_with_status(status) + + res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + assert res.data.get('days_left') == ANSWER_PERIOD # ======================================================================= # # Ordering & Filtering # # ======================================================================= # - + # 좋아요 개수로 정렬 확인 def test_ordering_by_positive_vote_count(self): # TODO: In different branch @@ -354,16 +355,11 @@ def test_admin_can_view_admin_api_list(self): def test_admin_can_view_admin_api_get(self): res = self.http_request(self.school_admin, 'get', f'communication_articles/{self.article.id}') - self.communication_article.refresh_from_db() assert res.status_code == 200 assert res.data.get('article') == self.communication_article.article.id - assert res.data.get('school_response_status') == self.communication_article.school_response_status def test_admin_can_update_admin_api(self): - current_time = timezone.now() - res = self.http_request(self.school_admin, 'put', f'communication_articles/{self.article.id}', { - 'confirmed_by_school_at': current_time, - }) + res = self.http_request(self.school_admin, 'put', f'communication_articles/{self.article.id}') assert res.status_code == 200 self.communication_article.refresh_from_db() cas = CommunicationArticleSerializer(self.communication_article) @@ -371,26 +367,12 @@ def test_admin_can_update_admin_api(self): def test_user_cannot_view_admin_api_list(self): res = self.http_request(self.user, 'get', 'communication_articles') - assert res.status_code == 403 + assert res.status_code == status.HTTP_403_FORBIDDEN def test_user_cannot_view_admin_api_get(self): res = self.http_request(self.user, 'get', f'communication_articles/{self.article.id}') - assert res.status_code == 403 + assert res.status_code == status.HTTP_403_FORBIDDEN def test_user_cannot_update_admin_api(self): - res = self.http_request(self.user, 'put', f'communication_articles/{self.article.id}', { - 'confirmed_by_school_at': timezone.now(), - }) - assert res.status_code == 403 - - def test_days_left(self): - days_left = 6 - current_time = timezone.now() - deadline = current_time + timedelta(days=days_left) - self.http_request(self.school_admin, 'put', f'communication_articles/{self.article.id}', { - 'confirmed_by_school_at': current_time, - 'response_deadline': deadline, - }) - res = self.http_request(self.school_admin, 'get', f'communication_articles/{self.article.id}') - return res.data.get('days_left') == days_left - + res = self.http_request(self.user, 'put', f'communication_articles/{self.article.id}') + assert res.status_code == status.HTTP_403_FORBIDDEN From dd31749969dbfe01595c6a2ee659e0f41058964f Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Fri, 6 May 2022 01:44:25 +0900 Subject: [PATCH 061/101] Squash migration files from 38-2 to 41 --- ...le.py => 0039_add_communication_article.py} | 11 +++++++++-- .../0039_board_is_school_communication.py | 18 ------------------ ...municationarticle_school_response_status.py | 18 ------------------ .../migrations/0041_merge_20220426_0146.py | 14 -------------- 4 files changed, 9 insertions(+), 52 deletions(-) rename apps/core/migrations/{0038_communicationarticle.py => 0039_add_communication_article.py} (73%) delete mode 100644 apps/core/migrations/0039_board_is_school_communication.py delete mode 100644 apps/core/migrations/0040_communicationarticle_school_response_status.py delete mode 100644 apps/core/migrations/0041_merge_20220426_0146.py diff --git a/apps/core/migrations/0038_communicationarticle.py b/apps/core/migrations/0039_add_communication_article.py similarity index 73% rename from apps/core/migrations/0038_communicationarticle.py rename to apps/core/migrations/0039_add_communication_article.py index 2741e53d..aa2279e2 100644 --- a/apps/core/migrations/0038_communicationarticle.py +++ b/apps/core/migrations/0039_add_communication_article.py @@ -1,5 +1,6 @@ -# Generated by Django 3.2.9 on 2022-03-28 11:45 +# Generated by Django 3.2.9 on 2022-05-05 16:34 +import apps.core.models.communication_article import datetime from django.db import migrations, models import django.db.models.deletion @@ -10,10 +11,15 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0037_board_banner_url'), + ('core', '0038_rename_is_anonymous_to_name_type'), ] operations = [ + migrations.AddField( + model_name='board', + name='is_school_communication', + field=models.BooleanField(db_index=True, default=False, help_text='학교 소통 게시판 글임을 표시', verbose_name='학교와의 소통 게시판'), + ), migrations.CreateModel( name='CommunicationArticle', fields=[ @@ -24,6 +30,7 @@ class Migration(migrations.Migration): ('response_deadline', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='답변 요청 기한')), ('confirmed_by_school_at', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='학교측 문의 확인 시각')), ('answered_at', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='학교측 답변을 받은 시각')), + ('school_response_status', models.SmallIntegerField(default=apps.core.models.communication_article.SchoolResponseStatus['BEFORE_UPVOTE_THRESHOLD'], verbose_name='답변 진행 상황')), ('article', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='communication_article', to='core.article', verbose_name='게시물')), ], options={ diff --git a/apps/core/migrations/0039_board_is_school_communication.py b/apps/core/migrations/0039_board_is_school_communication.py deleted file mode 100644 index ba592e26..00000000 --- a/apps/core/migrations/0039_board_is_school_communication.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.9 on 2022-03-28 11:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0038_communicationarticle'), - ] - - operations = [ - migrations.AddField( - model_name='board', - name='is_school_communication', - field=models.BooleanField(db_index=True, default=False, help_text='학교 소통 게시판 글임을 표시', verbose_name='학교와의 소통 게시판'), - ), - ] diff --git a/apps/core/migrations/0040_communicationarticle_school_response_status.py b/apps/core/migrations/0040_communicationarticle_school_response_status.py deleted file mode 100644 index 31fe2c79..00000000 --- a/apps/core/migrations/0040_communicationarticle_school_response_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.9 on 2022-03-31 18:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0039_board_is_school_communication'), - ] - - operations = [ - migrations.AddField( - model_name='communicationarticle', - name='school_response_status', - field=models.SmallIntegerField(default=0, verbose_name='답변 진행 상황'), - ), - ] diff --git a/apps/core/migrations/0041_merge_20220426_0146.py b/apps/core/migrations/0041_merge_20220426_0146.py deleted file mode 100644 index f304dac5..00000000 --- a/apps/core/migrations/0041_merge_20220426_0146.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.2.9 on 2022-04-25 16:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0038_rename_is_anonymous_to_name_type'), - ('core', '0040_communicationarticle_school_response_status'), - ] - - operations = [ - ] From 0f242c05e026de240262b7518dfd31a812076a3d Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Fri, 6 May 2022 02:00:45 +0900 Subject: [PATCH 062/101] add another users that have kaist_info or not --- tests/conftest.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2855059e..31f3e035 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,45 +22,63 @@ def set_admin_client(request): client.force_authenticate(user=request.cls.user) request.cls.api_client = client -# sso_user_info에 kaist_info 있음 + @pytest.fixture(scope='class') def set_user_client(request): request.cls.user, _ = User.objects.get_or_create(username='User', email='user@sparcs.org') if not hasattr(request.cls.user, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user, nickname='User', - sso_user_info={"kaist_info": "{\"ku_kname\": \"user\"}"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) client = APIClient() request.cls.api_client = client -# sso_user_info에 kaist_info 있음 + @pytest.fixture(scope='class') def set_user_client2(request): request.cls.user2, _ = User.objects.get_or_create(username='User2', email='user2@sparcs.org') if not hasattr(request.cls.user2, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user2, nickname='User2', - sso_user_info={"kaist_info": "{\"ku_kname\": \"user2\"}"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) request.cls.api_client = APIClient() -# sso_user_info에 kaist_info 없음 + @pytest.fixture(scope='class') def set_user_client3(request): request.cls.user3, _ = User.objects.get_or_create(username='User3', email='user3@sparcs.org') if not hasattr(request.cls.user3, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user3, nickname='User3', - sso_user_info={"kaist_info": None, "last_name": "User", "first_name": "3"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) request.cls.api_client = APIClient() -# sso_user_info에 kaist_info 없음 + @pytest.fixture(scope='class') def set_user_client4(request): request.cls.user4, _ = User.objects.get_or_create(username='User4', email='user4@sparcs.org') if not hasattr(request.cls.user4, 'profile'): UserProfile.objects.get_or_create(user=request.cls.user4, nickname='User4', - sso_user_info={"kaist_info": None, "last_name": "User", "first_name": "4"}, + group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) + + request.cls.api_client = APIClient() + + +@pytest.fixture(scope='class') +def set_user_with_kaist_info(request): + request.cls.user_with_kaist_info, _ = User.objects.get_or_create(username='User_with_kaist_info', email='user_with_kaist_info@sparcs.org') + if not hasattr(request.cls.user_with_kaist_info, 'profile'): + UserProfile.objects.get_or_create(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()) + + request.cls.api_client = APIClient() + + +@pytest.fixture(scope='class') +def set_user_without_kaist_info(request): + request.cls.user_without_kaist_info, _ = User.objects.get_or_create(username='User_without_kaist_info', email='user_without_kaist_info@sparcs.org') + if not hasattr(request.cls.user_without_kaist_info, 'profile'): + UserProfile.objects.get_or_create(user=request.cls.user_without_kaist_info, nickname='user_without_kinfo', + sso_user_info={"kaist_info": None, "last_name": "user_", "first_name": "without_kaist_info"}, group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) request.cls.api_client = APIClient() From 8e9542a6efeda2d12bb4e7044ae979b3af960e61 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Fri, 6 May 2022 02:06:42 +0900 Subject: [PATCH 063/101] fix realname article, comment tests --- tests/test_articles.py | 183 +++++++++++++++++++++---------------- tests/test_comments.py | 200 +++++++++++++++++++++-------------------- 2 files changed, 209 insertions(+), 174 deletions(-) diff --git a/tests/test_articles.py b/tests/test_articles.py index f4bbe674..9b89d688 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -1,6 +1,7 @@ import pytest from django.contrib.auth.models import User from django.utils import timezone +from rest_framework.test import APIClient from django.conf import settings @@ -11,18 +12,16 @@ @pytest.fixture(scope='class') -def set_board(request): +def set_boards(request): request.cls.board = Board.objects.create( slug="test board", ko_name="테스트 게시판", en_name="Test Board", ko_description="테스트 게시판입니다", - en_description="This is a board for testing" + en_description="This is a board for testing", + name_type=BoardNameType.REGULAR, ) - -@pytest.fixture(scope='class') -def set_anon_board(request): request.cls.anon_board = Board.objects.create( slug="anonymous", ko_name="익명 게시판", @@ -32,21 +31,18 @@ def set_anon_board(request): name_type=BoardNameType.ANONYMOUS ) - -@pytest.fixture(scope='class') -def set_realname_board(request): request.cls.realname_board = Board.objects.create( - slug="realname", - ko_name="실명 게시판", - en_name="Realname Board", - ko_description="실명 게시판", - en_description="Realname Board", - name_type=BoardNameType.REALNAME + slug="test realname board", + ko_name="테스트 실명 게시판", + en_name="Test realname Board", + ko_description="테스트 실명 게시판입니다", + en_description="This is a realname board for testing", + name_type=BoardNameType.REALNAME, ) @pytest.fixture(scope='class') -def set_topic(request): +def set_topics(request): """set_board 먼저 적용""" request.cls.topic = Topic.objects.create( slug="test topic", @@ -57,9 +53,18 @@ def set_topic(request): parent_board=request.cls.board ) + request.cls.realname_topic = Topic.objects.create( + slug="test realname topic", + ko_name="테스트 실명 토픽", + en_name="Test realname Topic", + ko_description="테스트용 실명 토픽입니다", + en_description="This is realname topic for testing", + parent_board=request.cls.realname_board + ) + @pytest.fixture(scope='class') -def set_article(request): +def set_articles(request): """set_board 먼저 적용""" request.cls.article = Article.objects.create( title="example article", @@ -129,7 +134,8 @@ def set_readonly_board(request): request.cls.readonly_board.delete() -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3','set_user_client4','set_board', 'set_anon_board', 'set_realname_board', 'set_topic', 'set_article') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', 'set_user_with_kaist_info', + 'set_boards', 'set_topics', 'set_articles') class TestArticle(TestCase, RequestSetting): def test_list(self): # article 개수를 확인하는 테스트 @@ -213,42 +219,6 @@ def test_anonymous_article(self): assert res2.get('name_type') == BoardNameType.ANONYMOUS assert res2.get('created_by')['username'] != anon_article.created_by.username - # http get으로 익명 게시글을 retrieve했을 때 작성자가 실명으로 나타나는지 확인 - def test_realname_article(self): - # 실명 게시글 생성 - realname_article = Article.objects.create( - title="example realname article", - content="example realname content", - content_text="example realname content text", - name_type=BoardNameType.REALNAME, - is_content_sexual=False, - is_content_social=False, - hit_count=0, - positive_vote_count=0, - negative_vote_count=0, - created_by=self.user, - parent_topic=None, - parent_board=self.realname_board, - commented_at=timezone.now() - ) - - # 익명 게시글을 GET할 때, 작성자의 정보가 실명으로 전달되는 것 확인 - res = self.http_request(self.user, 'get', f'articles/{realname_article.id}').data - assert res.get('name_type') == BoardNameType.REALNAME - assert res.get('created_by')['username'] == realname_article.created_by.profile.realname - - res2 = self.http_request(self.user2, 'get', f'articles/{realname_article.id}').data - assert res2.get('name_type') == BoardNameType.REALNAME - assert res2.get('created_by')['username'] == realname_article.created_by.profile.realname - - res3 = self.http_request(self.user3, 'get', f'articles/{realname_article.id}').data - assert res3.get('name_type') == BoardNameType.REALNAME - assert res3.get('created_by')['username'] == realname_article.created_by.profile.realname - - res4 = self.http_request(self.user4, 'get', f'articles/{realname_article.id}').data - assert res4.get('name_type') == BoardNameType.REALNAME - assert res4.get('created_by')['username'] == realname_article.created_by.profile.realname - def test_create(self): # test_create: HTTP request (POST)를 이용해서 생성 # user data in dict @@ -288,28 +258,6 @@ def test_create_anonymous(self): result = self.http_request(self.user, 'post', 'articles', user_data) assert not result.data['name_type'] == BoardNameType.ANONYMOUS - def test_create_realname(self): - user_data = { - "title": "article for test_create", - "content": "content for test_create", - "content_text": "content_text for test_create", - "is_content_sexual": False, - "is_content_social": False, - "parent_topic": None, - "parent_board": self.realname_board.id - } - - result = self.http_request(self.user, 'post', 'articles', user_data) - - assert result.data['name_type'] == BoardNameType.REALNAME - - user_data.update({ - "parent_topic": self.topic.id, - "parent_board": self.board.id - }) - result = self.http_request(self.user, 'post', 'articles', user_data) - assert not result.data['name_type'] == BoardNameType.REALNAME - def test_update_cache_sync(self): new_title = 'title changed!' new_content = 'content changed!' @@ -517,8 +465,91 @@ def test_deleting_with_comments(self): ).count() == 0 assert self.article.comment_count == 0 +@pytest.mark.usefixtures('set_user_client', 'set_user_with_kaist_info', 'set_user_without_kaist_info', + 'set_boards', 'set_topics', 'set_articles') +class TestRealnameArticle(TestCase, RequestSetting): + def test_get_realname_article(self): + # kaist info가 있는 유저가 생성한 게시글 + realname_article_with_kinfo = Article.objects.create( + title='example realname article with kinfo', + content='example realname content with kinfo', + content_text='example realname content text with kinfo', + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=self.user_with_kaist_info, + parent_topic=self.realname_topic, + parent_board=self.realname_board, + commented_at=timezone.now() + ) + + # kaist info가 없는 유저가 생성한 게시글 + realname_article_without_kinfo = Article.objects.create( + title='example realname article without_kinfo', + content='example realname content without_kinfo', + content_text='example realname content text without_kinfo', + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=self.user_without_kaist_info, + parent_topic=self.realname_topic, + parent_board=self.realname_board, + commented_at=timezone.now() + ) + + res = self.http_request(self.user_with_kaist_info, 'get', f'articles/{realname_article_with_kinfo.id}').data + assert res.get('name_type') == BoardNameType.REALNAME + assert res.get('created_by')['username'] == realname_article_with_kinfo.created_by.profile.realname + + res2 = self.http_request(self.user_without_kaist_info, 'get', f'articles/{realname_article_without_kinfo.id}').data + assert res2.get('name_type') == BoardNameType.REALNAME + assert res2.get('created_by')['username'] == realname_article_without_kinfo.created_by.profile.realname + + def test_create_realname_article(self): + article_title = 'realname article for test_create' + article_data = { + 'title': article_title, + 'content': 'realname content for test_create', + 'content_text': 'realname content_text for test_create', + 'is_content_sexual': False, + 'is_content_social': False, + 'parent_topic': self.realname_topic.id, + 'parent_board': self.realname_board.id + } + + result = self.http_request(self.user_with_kaist_info, 'post', 'articles', article_data).data + + assert result.get('name_type') == BoardNameType.REALNAME + assert Article.objects.get(title=article_title).name_type == BoardNameType.REALNAME + + def test_update_realname_article(self): + article_title = 'realname article for test_create' + article_data = { + 'title': article_title, + 'content': 'realname content for test_update', + 'content_text': 'realname content_text for test_update', + 'is_content_sexual': False, + 'is_content_social': False, + 'parent_topic': None, + 'parent_board': self.realname_board.id + } + + article_data.update({ + 'parent_topic': self.realname_topic.id + }) + result = self.http_request(self.user_with_kaist_info, 'post', 'articles', article_data).data + + assert result.get('name_type') == BoardNameType.REALNAME + assert Article.objects.get(title=article_title).name_type == BoardNameType.REALNAME -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_board', 'set_topic', 'set_article') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', 'set_user_without_kaist_info', + 'set_boards', 'set_topics', 'set_articles') class TestHiddenArticles(TestCase, RequestSetting): @staticmethod def _user_factory(user_kwargs, profile_kwargs): diff --git a/tests/test_comments.py b/tests/test_comments.py index 380fa61a..afc9c8d3 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -3,24 +3,38 @@ import pytest from django.contrib.auth.models import User from django.utils import timezone +from django.utils.translation import gettext +from rest_framework.test import APIClient + from apps.core.models import Article, Topic, Board, Comment, Block, Vote from apps.core.models.board import BoardNameType +from apps.user.models import UserProfile from tests.conftest import RequestSetting, TestCase @pytest.fixture(scope='class') -def set_board(request): +def set_boards(request): request.cls.board = Board.objects.create( slug='test board', ko_name='테스트 게시판', en_name='Test Board', + name_type=BoardNameType.REGULAR, ko_description='테스트 게시판입니다', en_description='This is a board for testing' ) + request.cls.realname_board = Board.objects.create( + slug='test realname board', + ko_name='테스트 실명 게시판', + en_name='Test realname Board', + name_type=BoardNameType.REALNAME, + ko_description='테스트 실명 게시판입니다', + en_description='This is a realname board for testing' + ) + @pytest.fixture(scope='class') -def set_topic(request): +def set_topics(request): """set_board 먼저 적용""" request.cls.topic = Topic.objects.create( slug='test topic', @@ -31,11 +45,20 @@ def set_topic(request): parent_board=request.cls.board ) + request.cls.realname_topic = Topic.objects.create( + slug='test realname topic', + ko_name='테스트 실명 토픽', + en_name='Test realname Topic', + ko_description='테스트용 실명 토픽입니다', + en_description='This is realname topic for testing', + parent_board=request.cls.realname_board + ) + @pytest.fixture(scope='class') def set_articles(request): """set_topic, set_user_client 먼저 적용""" - request.cls.article = Article.objects.create( + request.cls.article_regular = Article.objects.create( title='Test Article', content='Content of test article', content_text='Content of test article in text', @@ -68,9 +91,11 @@ def set_articles(request): commented_at=timezone.now() ) - request.cls.article_realname = Article.objects.create( + + """set_realname_topic, set_user_with_kaist_info 먼저 적용""" + request.cls.realname_article = Article.objects.create( title='Realname Test Article', - content='Content of test article', + content='Content of test realname article', content_text='Content of test article in text', name_type=BoardNameType.REALNAME, is_content_sexual=False, @@ -78,9 +103,9 @@ def set_articles(request): hit_count=0, positive_vote_count=0, negative_vote_count=0, - created_by=request.cls.user, - parent_topic=request.cls.topic, - parent_board=request.cls.board, + created_by=request.cls.user_with_kaist_info, + parent_topic=request.cls.realname_topic, + parent_board=request.cls.realname_board, commented_at=timezone.now() ) @@ -92,7 +117,7 @@ def set_comments(request): content='this is a test comment', name_type=BoardNameType.REGULAR, created_by=request.cls.user, - parent_article=request.cls.article, + parent_article=request.cls.article_regular, ) request.cls.comment_anonymous = Comment.objects.create( @@ -102,36 +127,37 @@ def set_comments(request): parent_article=request.cls.article_anonymous, ) - request.cls.comment_realname = Comment.objects.create( + request.cls.realname_comment = Comment.objects.create( content='this is an realname test comment', name_type=BoardNameType.REALNAME, - created_by=request.cls.user, - parent_article=request.cls.article_realname, + created_by=request.cls.user_with_kaist_info, + parent_article=request.cls.realname_article, ) -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', 'set_board', 'set_topic', 'set_articles', 'set_comments') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', 'set_user_with_kaist_info', + 'set_boards', 'set_topics', 'set_articles', 'set_comments') class TestComments(TestCase, RequestSetting): # comment 개수를 확인하는 테스트 def test_comment_list(self): # number of comments is initially 0 - res = self.http_request(self.user, 'get', f'articles/{self.article.id}') + res = self.http_request(self.user, 'get', f'articles/{self.article_regular.id}') assert res.data.get('comment_count') == 1 comment2 = Comment.objects.create( content='Test comment 2', name_type=BoardNameType.REGULAR, created_by=self.user, - parent_article=self.article + parent_article=self.article_regular ) comment3 = Comment.objects.create( content='Test comment 3', name_type=BoardNameType.REGULAR, created_by=self.user, - parent_article=self.article + parent_article=self.article_regular ) - res = self.http_request(self.user, 'get', f'articles/{self.article.id}') + res = self.http_request(self.user, 'get', f'articles/{self.article_regular.id}') assert res.data.get('comment_count') == 3 # post로 댓글 생성됨을 확인 @@ -139,7 +165,7 @@ def test_create_comment(self): comment_str = 'this is a test comment for test_create_comment' comment_data = { 'content': comment_str, - 'parent_article': self.article.id, + 'parent_article': self.article_regular.id, 'parent_comment': None, 'attachment': None, } @@ -160,7 +186,7 @@ def test_create_subcomment(self): # 댓글의 생성과 삭제에 따라서 article의 comment_count가 맞게 바뀌는지 확인 def test_article_comment_count(self): - article = Article.objects.get(id=self.article.id) + article = Article.objects.get(id=self.article_regular.id) assert article.comment_count == 1 comment = Comment.objects.get(id=self.comment.id) comment.delete() @@ -170,7 +196,7 @@ def test_article_comment_count(self): # 대댓글의 생성과 삭제에 따라서 article의 comment_count가 맞게 바뀌는지 확인 def test_article_comment_count_with_subcomments(self): - article = Article.objects.get(id=self.article.id) + article = Article.objects.get(id=self.article_regular.id) print('comment set: ', article.comment_set) assert article.comment_count == 1 @@ -181,7 +207,7 @@ def test_article_comment_count_with_subcomments(self): parent_comment=self.comment ) - article = Article.objects.get(id=self.article.id) + article = Article.objects.get(id=self.article_regular.id) print('comment set: ', article.comment_set) assert article.comment_count == 2 @@ -192,7 +218,7 @@ def test_article_comment_count_with_subcomments(self): parent_comment=self.comment ) - article = Article.objects.get(id=self.article.id) + article = Article.objects.get(id=self.article_regular.id) print('comment set: ', article.comment_set) assert article.comment_count == 3 @@ -258,7 +284,7 @@ def test_anonymous_comment(self): content='Anonymous test comment', name_type=BoardNameType.ANONYMOUS, created_by=self.user, - parent_article=self.article + parent_article=self.article_regular ) # 익명 댓글을 GET할 때, 작성자의 정보가 전달되지 않는 것 확인 @@ -277,7 +303,7 @@ def test_anonymous_comment_by_article_writer(self): content='Anonymous test comment', name_type=BoardNameType.ANONYMOUS, created_by=self.user, - parent_article=self.article + parent_article=self.article_anonymous ) r_article = self.http_request(self.user, 'get', f'articles/{self.article_anonymous.id}').data @@ -292,59 +318,11 @@ def test_anonymous_comment_by_article_writer(self): comment_writer_id2 = r_comment2.get('created_by')['id'] assert article_writer_id2 == comment_writer_id2 - # http get으로 실명 댓글을 retrieve했을 때 작성자가 실명으로 나타나는지 확인 - def test_realname_comment(self): - # 실명 댓글 생성 - realname_comment = Comment.objects.create( - content='Realname test comment', - name_type=BoardNameType.REALNAME, - created_by=self.user, - parent_article=self.article - ) - - # 익명 댓글을 GET할 때, 작성자의 정보가 전달되지 않는 것 확인 - res = self.http_request(self.user, 'get', f'comments/{realname_comment.id}').data - assert res.get('name_type') == BoardNameType.REALNAME - assert res.get('created_by')['username'] != realname_comment.created_by.profile.realname - - res2 = self.http_request(self.user2, 'get', f'comments/{realname_comment.id}').data - assert res2.get('name_type') == BoardNameType.REALNAME - assert res2.get('created_by')['username'] != realname_comment.created_by.profile.realname - - res3 = self.http_request(self.user3, 'get', f'comments/{realname_comment.id}').data - assert res3.get('name_type') == BoardNameType.REALNAME - assert res3.get('created_by')['username'] != realname_comment.created_by.profile.realname - - res4 = self.http_request(self.user4, 'get', f'comments/{realname_comment.id}').data - assert res4.get('name_type') == BoardNameType.REALNAME - assert res4.get('created_by')['username'] != realname_comment.created_by.profile.realname - - def test_realname_comment_by_article_writer(self): - # 익명 댓글 생성 - Comment.objects.create( - content='Anonymous test comment', - name_type=BoardNameType.REALNAME, - created_by=self.user, - parent_article=self.article - ) - - r_article = self.http_request(self.user, 'get', f'articles/{self.article_realname.id}').data - article_writer_id = r_article.get('created_by')['id'] - r_comment = self.http_request(self.user, 'get', f'comments/{self.comment_realname.id}').data - comment_writer_id = r_comment.get('created_by')['id'] - assert article_writer_id == comment_writer_id - - r_article2 = self.http_request(self.user2, 'get', f'articles/{self.article_realname.id}').data - article_writer_id2 = r_article2.get('created_by')['id'] - r_comment2 = self.http_request(self.user2, 'get', f'comments/{self.comment_realname.id}').data - comment_writer_id2 = r_comment2.get('created_by')['id'] - assert article_writer_id2 == comment_writer_id2 - def test_comment_on_regular_parent_article(self): comment_str = 'This is a comment on a regular parent article' comment_data = { 'content': comment_str, - 'parent_article': self.article.id, + 'parent_article': self.article_regular.id, 'parent_comment': None, 'attachment': None, } @@ -385,28 +363,6 @@ def test_comment_on_anonymous_parent_comment(self): self.http_request(self.user, 'post', 'comments', comment_data) assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS - def test_comment_on_realname_parent_article(self): - comment_str = 'This is a comment on a realname parent article' - comment_data = { - 'content': comment_str, - 'parent_article': self.article_realname.id, - 'parent_comment': None, - 'attachment': None, - } - self.http_request(self.user, 'post', 'comments', comment_data) - assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.REALNAME - - def test_comment_on_realname_parent_comment(self): - comment_str = 'This is a comment on a realname parent comment' - comment_data = { - 'content': comment_str, - 'parent_article': None, - 'parent_comment': self.comment_realname.id, - 'attachment': None, - } - self.http_request(self.user, 'post', 'comments', comment_data) - assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.REALNAME - # 댓글 좋아요 확인 def test_positive_vote(self): # 좋아요 2표 @@ -452,14 +408,62 @@ def test_cannot_vote_both(self): assert comment.negative_vote_count == 1 -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_board', 'set_topic', 'set_articles', 'set_comments') +@pytest.mark.usefixtures('set_user_client', 'set_user_with_kaist_info', 'set_user_without_kaist_info', + 'set_boards', 'set_topics', 'set_articles', 'set_comments') +class TestRealnameComments(TestCase, RequestSetting): + def test_get_realname_comment(self): + comment = Comment.objects.create( + content='Realname test comment', + name_type=BoardNameType.REALNAME, + created_by=self.user_without_kaist_info, + parent_article=self.realname_article + ) + + res = self.http_request(self.user_without_kaist_info, 'get', f'comments/{comment.id}').data + assert res.get('name_type') == BoardNameType.REALNAME + assert res.get('created_by')['username'] == comment.created_by.profile.realname + + def test_get_realname_comment_by_article_writer(self): + res = self.http_request(self.user_with_kaist_info, 'get', f'comments/{self.realname_comment.id}').data + assert res.get('name_type') == BoardNameType.REALNAME + assert res.get('created_by')['username'] == gettext('author') + + def test_create_realname_comment(self): + comment_str = 'This is a comment on a realname article' + comment_data = { + 'content': comment_str, + 'parent_article': self.realname_article.id, + 'parent_comment': None, + 'attachment': None, + } + + res = self.http_request(self.user_with_kaist_info, 'post', 'comments', comment_data).data + assert res.get('name_type') == BoardNameType.REALNAME + assert Comment.objects.get(content=comment_str).name_type == BoardNameType.REALNAME + + def test_create_realname_subcomment(self): + comment_str = 'This is a subcomment on a realname comment' + comment_data = { + 'content': comment_str, + 'parent_article': None, + 'parent_comment': self.realname_comment.id, + 'attachment': None, + } + + res = self.http_request(self.user_with_kaist_info, 'post', 'comments', comment_data).data + assert res.get('name_type') == BoardNameType.REALNAME + assert Comment.objects.get(content=comment_str).name_type == BoardNameType.REALNAME + + +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', + 'set_boards', 'set_topics', 'set_articles', 'set_comments') class TestHiddenComments(TestCase, RequestSetting): def _comment_factory(self, **comment_kwargs): return Comment.objects.create( content='example comment', name_type=BoardNameType.REGULAR, created_by=self.user, - parent_article=self.article, + parent_article=self.article_regular, **comment_kwargs ) @@ -483,7 +487,7 @@ def test_blocked_user_block(self): content='example comment', name_type=BoardNameType.REGULAR, created_by=self.user2, - parent_article=self.article + parent_article=self.article_regular ) res = self.http_request(self.user, 'get', f'comments/{comment2.id}').data From 088deec46bd023824695cdc574d41f054500dbdf Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Fri, 6 May 2022 01:44:25 +0900 Subject: [PATCH 064/101] Squash migration files from 38-2 to 41 --- ...le.py => 0039_add_communication_article.py} | 11 +++++++++-- .../0039_board_is_school_communication.py | 18 ------------------ ...municationarticle_school_response_status.py | 18 ------------------ .../migrations/0041_merge_20220426_0146.py | 14 -------------- 4 files changed, 9 insertions(+), 52 deletions(-) rename apps/core/migrations/{0038_communicationarticle.py => 0039_add_communication_article.py} (73%) delete mode 100644 apps/core/migrations/0039_board_is_school_communication.py delete mode 100644 apps/core/migrations/0040_communicationarticle_school_response_status.py delete mode 100644 apps/core/migrations/0041_merge_20220426_0146.py diff --git a/apps/core/migrations/0038_communicationarticle.py b/apps/core/migrations/0039_add_communication_article.py similarity index 73% rename from apps/core/migrations/0038_communicationarticle.py rename to apps/core/migrations/0039_add_communication_article.py index 2741e53d..aa2279e2 100644 --- a/apps/core/migrations/0038_communicationarticle.py +++ b/apps/core/migrations/0039_add_communication_article.py @@ -1,5 +1,6 @@ -# Generated by Django 3.2.9 on 2022-03-28 11:45 +# Generated by Django 3.2.9 on 2022-05-05 16:34 +import apps.core.models.communication_article import datetime from django.db import migrations, models import django.db.models.deletion @@ -10,10 +11,15 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0037_board_banner_url'), + ('core', '0038_rename_is_anonymous_to_name_type'), ] operations = [ + migrations.AddField( + model_name='board', + name='is_school_communication', + field=models.BooleanField(db_index=True, default=False, help_text='학교 소통 게시판 글임을 표시', verbose_name='학교와의 소통 게시판'), + ), migrations.CreateModel( name='CommunicationArticle', fields=[ @@ -24,6 +30,7 @@ class Migration(migrations.Migration): ('response_deadline', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='답변 요청 기한')), ('confirmed_by_school_at', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='학교측 문의 확인 시각')), ('answered_at', models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=utc), verbose_name='학교측 답변을 받은 시각')), + ('school_response_status', models.SmallIntegerField(default=apps.core.models.communication_article.SchoolResponseStatus['BEFORE_UPVOTE_THRESHOLD'], verbose_name='답변 진행 상황')), ('article', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='communication_article', to='core.article', verbose_name='게시물')), ], options={ diff --git a/apps/core/migrations/0039_board_is_school_communication.py b/apps/core/migrations/0039_board_is_school_communication.py deleted file mode 100644 index ba592e26..00000000 --- a/apps/core/migrations/0039_board_is_school_communication.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.9 on 2022-03-28 11:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0038_communicationarticle'), - ] - - operations = [ - migrations.AddField( - model_name='board', - name='is_school_communication', - field=models.BooleanField(db_index=True, default=False, help_text='학교 소통 게시판 글임을 표시', verbose_name='학교와의 소통 게시판'), - ), - ] diff --git a/apps/core/migrations/0040_communicationarticle_school_response_status.py b/apps/core/migrations/0040_communicationarticle_school_response_status.py deleted file mode 100644 index 31fe2c79..00000000 --- a/apps/core/migrations/0040_communicationarticle_school_response_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.9 on 2022-03-31 18:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0039_board_is_school_communication'), - ] - - operations = [ - migrations.AddField( - model_name='communicationarticle', - name='school_response_status', - field=models.SmallIntegerField(default=0, verbose_name='답변 진행 상황'), - ), - ] diff --git a/apps/core/migrations/0041_merge_20220426_0146.py b/apps/core/migrations/0041_merge_20220426_0146.py deleted file mode 100644 index f304dac5..00000000 --- a/apps/core/migrations/0041_merge_20220426_0146.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.2.9 on 2022-04-25 16:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0038_rename_is_anonymous_to_name_type'), - ('core', '0040_communicationarticle_school_response_status'), - ] - - operations = [ - ] From ce2735f72026bdaae90e51394388dff8ae4ed769 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Fri, 6 May 2022 02:41:15 +0900 Subject: [PATCH 065/101] Squash migration files from 41 to 43 --- ... 0040_alter_communication_article_field.py} | 9 +++++++-- .../migrations/0042_alter_board_access_mask.py | 18 ------------------ .../migrations/0043_merge_20220429_0116.py | 14 -------------- 3 files changed, 7 insertions(+), 34 deletions(-) rename apps/core/migrations/{0041_communication_article_primary_key.py => 0040_alter_communication_article_field.py} (67%) delete mode 100644 apps/core/migrations/0042_alter_board_access_mask.py delete mode 100644 apps/core/migrations/0043_merge_20220429_0116.py diff --git a/apps/core/migrations/0041_communication_article_primary_key.py b/apps/core/migrations/0040_alter_communication_article_field.py similarity index 67% rename from apps/core/migrations/0041_communication_article_primary_key.py rename to apps/core/migrations/0040_alter_communication_article_field.py index a4f8230c..6d6b3feb 100644 --- a/apps/core/migrations/0041_communication_article_primary_key.py +++ b/apps/core/migrations/0040_alter_communication_article_field.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.9 on 2022-04-04 12:49 +# Generated by Django 3.2.9 on 2022-05-05 17:37 from django.db import migrations, models import django.db.models.deletion @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0040_communicationarticle_school_response_status'), + ('core', '0039_add_communication_article'), ] operations = [ @@ -15,6 +15,11 @@ class Migration(migrations.Migration): model_name='communicationarticle', name='id', ), + migrations.AlterField( + model_name='board', + name='access_mask', + field=models.IntegerField(default=222, verbose_name='접근 권한 값'), + ), migrations.AlterField( model_name='communicationarticle', name='article', diff --git a/apps/core/migrations/0042_alter_board_access_mask.py b/apps/core/migrations/0042_alter_board_access_mask.py deleted file mode 100644 index dee95abc..00000000 --- a/apps/core/migrations/0042_alter_board_access_mask.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.9 on 2022-04-24 14:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0041_communication_article_primary_key'), - ] - - operations = [ - migrations.AlterField( - model_name='board', - name='access_mask', - field=models.IntegerField(default=223, verbose_name='접근 권한 값'), - ), - ] diff --git a/apps/core/migrations/0043_merge_20220429_0116.py b/apps/core/migrations/0043_merge_20220429_0116.py deleted file mode 100644 index e848e01b..00000000 --- a/apps/core/migrations/0043_merge_20220429_0116.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.2.9 on 2022-04-28 16:16 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0041_merge_20220426_0146'), - ('core', '0042_alter_board_access_mask'), - ] - - operations = [ - ] From d2aaa212567c99750249e8335c47c4855d38880c Mon Sep 17 00:00:00 2001 From: kjy2844 Date: Mon, 9 May 2022 14:17:39 +0900 Subject: [PATCH 066/101] Fix PR #314 review --- apps/core/models/article.py | 10 ++-- apps/core/serializers/article.py | 2 +- .../views/viewsets/communication_article.py | 2 +- tests/test_communication_article.py | 59 ++++++++++++++----- 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 0940e5c9..f687a578 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -199,11 +199,11 @@ def update_vote_status(self): self.positive_vote_count = self.vote_set.filter(is_positive=True).count() + self.migrated_positive_vote_count self.negative_vote_count = self.vote_set.filter(is_positive=False).count() + self.migrated_negative_vote_count - if self.parent_board.is_school_communication: - if self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD and self.communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: - self.communication_article.school_response_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM - self.communication_article.response_deadline = ( - timezone.localtime() + timezone.timedelta(days=ANSWER_PERIOD + 1)).date() + if self.parent_board.is_school_communication and \ + self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD and \ + self.communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: + self.communication_article.school_response_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + self.communication_article.response_deadline = (timezone.localtime() + timezone.timedelta(days=ANSWER_PERIOD + 1)).date() self.communication_article.save() self.save() diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 1ef4350b..add607fa 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -346,7 +346,7 @@ class ArticleAttachmentType(Enum): BOTH = 'BOTH' -class ArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): +class ArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): parent_topic = TopicSerializer( read_only=True, ) diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index 9b0d2718..3de0cb99 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -32,7 +32,7 @@ class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): # 학교 담당자가 신문고 게시글에 대해 `확인했습니다` 버튼을 누른 경우 def update(self, request, *args, **kwargs): # 이미 확인했습니다 단계를 지나간 경우 - if self.get_object().school_response_status > SchoolResponseStatus.PREPARING_ANSWER: + if self.get_object().school_response_status >= SchoolResponseStatus.PREPARING_ANSWER: return response.Response(status=status.HTTP_200_OK) return super().update(request, *args, **kwargs) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 324404bd..8017bf5c 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -1,3 +1,7 @@ +import sys +from datetime import timedelta +from unittest.mock import patch + import pytest from django.utils import timezone @@ -100,8 +104,8 @@ def _add_upvotes(self, article, users): for user in users: self.http_request(user, 'post', f'articles/{article.id}/vote_positive') - def _confirm_communication_article(self, article): - self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}') + def _confirm_communication_article(self, article, user): + return self.http_request(user, 'put', f'communication_articles/{article.id}') def _add_admin_comment(self, article): comment_data = { @@ -122,9 +126,9 @@ def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_ 'parent_board': self.communication_board.id, 'name_type': BoardNameType.REALNAME } - self.http_request(self.user, 'post', 'articles', article_data) + res = self.http_request(self.user, 'post', 'articles', article_data) - article = Article.objects.get(title=article_title) + article = Article.objects.get(id=res.data.get('id')) if status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: pass @@ -132,10 +136,7 @@ def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_ users_tuple = (self.user2, self.user3, self.user4) self._add_upvotes(article, users_tuple) elif status == SchoolResponseStatus.PREPARING_ANSWER: - # 작성일 기준 status 변경 방법이 존재하지 않음 - users_tuple = (self.user2, self.user3, self.user4) - self._add_upvotes(article, users_tuple) - self._confirm_communication_article(article) + self._confirm_communication_article(article, self.school_admin) elif status == SchoolResponseStatus.ANSWER_DONE: self._add_admin_comment(article) @@ -215,7 +216,7 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_PREPARING_ANSWER(self): article = self._create_article_with_status(from_status) assert self._get_communication_article_status(article) == from_status - self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}') + self._confirm_communication_article(article, self.school_admin) assert self._get_communication_article_status(article) == to_status # 0 -> 3 @@ -237,7 +238,7 @@ def test_BEFORE_SCHOOL_CONFIRM_to_PREPARING_ANSWER(self): article = self._create_article_with_status(from_status) assert self._get_communication_article_status(article) == from_status - self.http_request(self.school_admin, 'put', f'communication_articles/{article.id}') + self._confirm_communication_article(article, self.school_admin) assert self._get_communication_article_status(article) == to_status # 1 -> 3 @@ -269,6 +270,12 @@ def test_PREPARING_ANSWER_to_BEFORE_SCHOOL_CONFIRM(self): article = self._create_article_with_status(status) assert self._get_communication_article_status(article) == status + # 좋아요 수가 threshold를 넘었을 때 + for user in (self.user2, self.user3, self.user4): + self.http_request(user, 'post', f'articles/{article.id}/vote_positive') + assert self._get_communication_article_status(article) == status + + # 좋아요 수가 threshold 밑으로 내려갔다가 다시 올라갈 때 # 정책상 소통게시판에서 vote_cancel은 허용되지 않으나, 테스트 정확성을 위해 첨부함 self.http_request(self.user4, 'post', f'articles/{article.id}/vote_cancel') self.http_request(self.user4, 'post', f'articles/{article.id}/vote_positive') @@ -292,16 +299,40 @@ def test_ANSWER_DONE_to_PREPARING_ANSWER(self): article = self._create_article_with_status(status) assert self._get_communication_article_status(article) == status - self._confirm_communication_article(article) + self._confirm_communication_article(article, self.school_admin) assert self._get_communication_article_status(article) == status def test_days_left(self): - status = SchoolResponseStatus.PREPARING_ANSWER + status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD article = self._create_article_with_status(status) + # days_left가 설정되기 전 + res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + assert res.data.get('days_left') == sys.maxsize + + user_tuple = (self.user2, self.user3, self.user4) + self._add_upvotes(article, user_tuple) res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') assert res.data.get('days_left') == ANSWER_PERIOD + def test_days_left_in_local_timezone(self): + status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM + article = self._create_article_with_status(status) + + today = timezone.localtime().replace(hour=23, minute=59, second=0) + tomorrow = (timezone.localtime() + timedelta(days=1)).replace(hour=0, minute=1, second=0) + + # localtime 기준으로 오늘에서 내일로 넘어갈 때 D-day 변경되는지 확인 + with patch.object(timezone, 'localtime', return_value=today): + res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + assert res.data.get('days_left') == ANSWER_PERIOD + + with patch.object(timezone, 'localtime', return_value=tomorrow): + res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + assert res.data.get('days_left') == ANSWER_PERIOD - 1 + + + # ======================================================================= # # Ordering & Filtering # # ======================================================================= # @@ -359,7 +390,7 @@ def test_admin_can_view_admin_api_get(self): assert res.data.get('article') == self.communication_article.article.id def test_admin_can_update_admin_api(self): - res = self.http_request(self.school_admin, 'put', f'communication_articles/{self.article.id}') + res = self._confirm_communication_article(self.article, self.school_admin) assert res.status_code == 200 self.communication_article.refresh_from_db() cas = CommunicationArticleSerializer(self.communication_article) @@ -374,5 +405,5 @@ def test_user_cannot_view_admin_api_get(self): assert res.status_code == status.HTTP_403_FORBIDDEN def test_user_cannot_update_admin_api(self): - res = self.http_request(self.user, 'put', f'communication_articles/{self.article.id}') + res = self._confirm_communication_article(self.article, self.user) assert res.status_code == status.HTTP_403_FORBIDDEN From a9de6845b4e74231659351106b6e7d2c91f839f2 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Fri, 6 May 2022 02:07:15 +0900 Subject: [PATCH 067/101] add realname user test --- tests/test_user.py | 68 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/tests/test_user.py b/tests/test_user.py index 7f5b314b..bf9287a8 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -11,7 +11,7 @@ @pytest.fixture(scope='class') -def set_board(request): +def set_boards(request): request.cls.board = Board.objects.create( slug="test board", ko_name="테스트 게시판", @@ -20,6 +20,15 @@ def set_board(request): en_description="This is a board for testing" ) + request.cls.realname_board = Board.objects.create( + slug="realname", + ko_name="실명 게시판", + en_name="Realname Board", + ko_description="실명 게시판", + en_description="Realname Board", + name_type=BoardNameType.REALNAME + ) + @pytest.fixture(scope='class') def set_articles(request): @@ -85,6 +94,22 @@ def set_anonymous_article(request): ) +@pytest.fixture(scope='class') +def set_realname_article(request): + """set_realname_board, set_user_with_kaist_info 먼저 적용""" + request.cls.realname_article = Article.objects.create( + title='실명글', + is_content_sexual=False, + is_content_social=False, + name_type=BoardNameType.REALNAME, + content='example content', + content_text='example content text', + created_by=request.cls.user_with_kaist_info, + parent_board=request.cls.realname_board, + hit_count=0, + ) + + @pytest.fixture(scope='class') def set_anonymous_comment(request): """set_anonymous_articles 먼저 적용""" @@ -96,7 +121,19 @@ def set_anonymous_comment(request): ) -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_board', 'set_articles') +@pytest.fixture(scope='class') +def set_realname_comment(request): + """set_realname_article, set_user_with_kaist_info 먼저 적용""" + request.cls.realname_comment = Comment.objects.create( + content='example comment', + name_type=BoardNameType.REALNAME, + created_by=request.cls.user_with_kaist_info, + parent_article=request.cls.realname_article, + ) + + +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', + 'set_boards', 'set_articles') class TestUser(TestCase, RequestSetting): def test_profile_edit(self): # 프로필 (ie. 사용자 설정)이 잘 변경되는지 테스트합니다. @@ -197,13 +234,14 @@ def test_nickname_update(self): assert (timezone.now() - self.user.profile.nickname_updated_at).total_seconds() < 5 -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_board', 'set_anonymous_article', 'set_anonymous_comment') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', + 'set_boards', 'set_anonymous_article', 'set_anonymous_comment') class TestAnonymousUser(TestCase, RequestSetting): def test_anonymous_article_profile_picture(self): r = self.http_request(self.user, 'get', f'articles/{self.article_anonymous.id}').data profile_picture_url = r.get('created_by')['profile']['picture'] try: - URLValidator(profile_picture_url) + URLValidator()(profile_picture_url) except ValidationError: assert False, 'Bad URL for anonymous article profile picture' @@ -211,6 +249,26 @@ def test_anonymous_comment_profile_picture(self): r = self.http_request(self.user, 'get', f'comments/{self.comment_anonymous.id}').data profile_picture_url = r.get('created_by')['profile']['picture'] try: - URLValidator(profile_picture_url) + URLValidator()(profile_picture_url) + except ValidationError: + assert False, 'Bad URL for anonymous comment profile picture' + + +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', + 'set_boards', 'set_realname_article', 'set_realname_comment') +class TestRealnameUser(TestCase, RequestSetting): + def test_realname_article_profile_picture(self): + r = self.http_request(self.user_with_kaist_info, 'get', f'articles/{self.realname_article.id}').data + profile_picture_url = r.get('created_by')['profile']['picture'] + try: + URLValidator()(profile_picture_url) + except ValidationError: + assert False, 'Bad URL for anonymous article profile picture' + + def test_realname_comment_profile_picture(self): + r = self.http_request(self.user_with_kaist_info, 'get', f'comments/{self.realname_comment.id}').data + profile_picture_url = r.get('created_by')['profile']['picture'] + try: + URLValidator()(profile_picture_url) except ValidationError: assert False, 'Bad URL for anonymous comment profile picture' From 3dcf2f46ba929e72b36cf74e30a3605289cd6be6 Mon Sep 17 00:00:00 2001 From: kjy2844 Date: Mon, 9 May 2022 21:45:02 +0900 Subject: [PATCH 068/101] Fix use _add_upvotes method --- apps/core/serializers/article.py | 2 +- tests/test_communication_article.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index add607fa..1ef4350b 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -346,7 +346,7 @@ class ArticleAttachmentType(Enum): BOTH = 'BOTH' -class ArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): +class ArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): parent_topic = TopicSerializer( read_only=True, ) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 8017bf5c..29e5d080 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -271,8 +271,8 @@ def test_PREPARING_ANSWER_to_BEFORE_SCHOOL_CONFIRM(self): assert self._get_communication_article_status(article) == status # 좋아요 수가 threshold를 넘었을 때 - for user in (self.user2, self.user3, self.user4): - self.http_request(user, 'post', f'articles/{article.id}/vote_positive') + users_tuple = (self.user2, self.user3, self.user4) + self._add_upvotes(article, users_tuple) assert self._get_communication_article_status(article) == status # 좋아요 수가 threshold 밑으로 내려갔다가 다시 올라갈 때 From 32d933fb304c18fc381df55256b4d0139839fe00 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 9 May 2022 22:02:35 +0900 Subject: [PATCH 069/101] Fix PR review Resolves: #316 --- apps/core/views/router.py | 6 ++++++ apps/core/views/viewsets/__init__.py | 1 + .../views/viewsets/communication_article.py | 20 +++++++++++++++++++ tests/test_communication_article.py | 7 +++---- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 apps/core/views/viewsets/communication_article.py diff --git a/apps/core/views/router.py b/apps/core/views/router.py index 1ec37c6e..13f27c65 100644 --- a/apps/core/views/router.py +++ b/apps/core/views/router.py @@ -63,3 +63,9 @@ prefix=r'best_searches', viewset=viewsets.BestSearchViewSet, ) + +# CommunicationArticleViewSet +router.register( + prefix=r'communication_articles', + viewset=viewsets.CommunicationArticleViewSet, +) diff --git a/apps/core/views/viewsets/__init__.py b/apps/core/views/viewsets/__init__.py index 89e9160d..a280de75 100644 --- a/apps/core/views/viewsets/__init__.py +++ b/apps/core/views/viewsets/__init__.py @@ -8,3 +8,4 @@ from .scrap import * from .faq import * from .best_search import * +from .communication_article import * diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py new file mode 100644 index 00000000..6898e054 --- /dev/null +++ b/apps/core/views/viewsets/communication_article.py @@ -0,0 +1,20 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, viewsets + +from ara.classes.viewset import ActionAPIViewSet + +from apps.core.models.communication_article import CommunicationArticle +from apps.core.serializers.communication_article import CommunicationArticleSerializer + + +class CommunicationArticleViewSet(viewsets.ReadOnlyModelViewSet, ActionAPIViewSet): + queryset = CommunicationArticle.objects.all() + serializer_class = CommunicationArticleSerializer + filter_backends = [filters.OrderingFilter, DjangoFilterBackend] + + # usage: /api/communication_articles/?ordering=created_at + ordering_fields = ['article__positive_vote_count'] + ordering = ['-article__positive_vote_count'] # default: 추천수 내림차순 + + # usage: /api/communication_articles/?school_response_status=1 + filterset_fields = ['school_response_status'] diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index a4ff73f0..d0d02e42 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -301,8 +301,7 @@ def test_descending_ordering_by_positive_vote_count(self): for vote_cnt, article in zip(vote_counts, articles): self._add_positive_votes(article, vote_cnt) - article.refresh_from_db() - + res = self.http_request(self.user, 'get', 'articles', querystring='ordering=-positive_vote_count,-created_at') assert res.status_code == HTTP_200_OK @@ -324,13 +323,13 @@ def test_ascending_ordering_by_positive_vote_count(self): for vote_cnt, article in zip(vote_counts, articles): self._add_positive_votes(article, vote_cnt) - article.refresh_from_db() - + res = self.http_request(self.user, 'get', 'articles', querystring='ordering=positive_vote_count,-created_at') assert res.status_code == HTTP_200_OK res_result = res.data.get('results') + assert Article.objects.count() == len(res_result) # 좋아요 개수 오름차순 정렬 확인 res_positive_votes = [el.get('positive_vote_count') for el in res_result] From c7527e61a133135b2804c6cd92e2b4b93513fa74 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Mon, 9 May 2022 22:29:39 +0900 Subject: [PATCH 070/101] Fix tests for articles and comments --- tests/test_articles.py | 38 +++++++++++++++++++++-------------- tests/test_comments.py | 45 +++++++++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/tests/test_articles.py b/tests/test_articles.py index 9b89d688..cc8ecb71 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -529,24 +529,32 @@ def test_create_realname_article(self): assert Article.objects.get(title=article_title).name_type == BoardNameType.REALNAME def test_update_realname_article(self): - article_title = 'realname article for test_create' - article_data = { - 'title': article_title, - 'content': 'realname content for test_update', - 'content_text': 'realname content_text for test_update', - 'is_content_sexual': False, - 'is_content_social': False, - 'parent_topic': None, - 'parent_board': self.realname_board.id - } + article = Article.objects.create( + title='realname article for test_create', + content='realname content for test_create', + content_text='realname content_text for test_create', + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=self.user_with_kaist_info, + parent_topic=self.realname_topic, + parent_board=self.realname_board, + commented_at=timezone.now() + ) + article.save() - article_data.update({ - 'parent_topic': self.realname_topic.id - }) - result = self.http_request(self.user_with_kaist_info, 'post', 'articles', article_data).data + new_title = 'realname article for test_update' + new_content = 'realname content for test_update' + result = self.http_request(self.user_with_kaist_info, 'put', f'articles/{article.id}', { + 'title': new_title, + 'content': new_content + }).data assert result.get('name_type') == BoardNameType.REALNAME - assert Article.objects.get(title=article_title).name_type == BoardNameType.REALNAME + assert Article.objects.get(title=new_title).name_type == BoardNameType.REALNAME @pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', 'set_user_without_kaist_info', 'set_boards', 'set_topics', 'set_articles') diff --git a/tests/test_comments.py b/tests/test_comments.py index afc9c8d3..e8358abc 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -110,6 +110,24 @@ def set_articles(request): ) + """set_realname_topic, set_user_without_kaist_info 먼저 적용""" + request.cls.realname_article_without_kinfo = Article.objects.create( + title='Realname Test Article', + content='Content of test realname article', + content_text='Content of test article in text', + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=request.cls.user_without_kaist_info, + parent_topic=request.cls.realname_topic, + parent_board=request.cls.realname_board, + commented_at=timezone.now() + ) + + @pytest.fixture(scope='class') def set_comments(request): """set_article 먼저 적용""" @@ -135,7 +153,8 @@ def set_comments(request): ) -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', 'set_user_with_kaist_info', +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', + 'set_user_with_kaist_info', 'set_user_without_kaist_info', 'set_boards', 'set_topics', 'set_articles', 'set_comments') class TestComments(TestCase, RequestSetting): # comment 개수를 확인하는 테스트 @@ -412,16 +431,27 @@ def test_cannot_vote_both(self): 'set_boards', 'set_topics', 'set_articles', 'set_comments') class TestRealnameComments(TestCase, RequestSetting): def test_get_realname_comment(self): - comment = Comment.objects.create( - content='Realname test comment', + comment1 = Comment.objects.create( + content='Realname test comment1', name_type=BoardNameType.REALNAME, created_by=self.user_without_kaist_info, parent_article=self.realname_article ) - res = self.http_request(self.user_without_kaist_info, 'get', f'comments/{comment.id}').data - assert res.get('name_type') == BoardNameType.REALNAME - assert res.get('created_by')['username'] == comment.created_by.profile.realname + comment2 = Comment.objects.create( + content='Realname test comment2', + name_type=BoardNameType.REALNAME, + created_by=self.user_with_kaist_info, + parent_article=self.realname_article_without_kinfo + ) + + res1 = self.http_request(self.user_without_kaist_info, 'get', f'comments/{comment1.id}').data + assert res1.get('name_type') == BoardNameType.REALNAME + assert res1.get('created_by')['username'] == comment1.created_by.profile.realname + + res2 = self.http_request(self.user_with_kaist_info, 'get', f'comments/{comment2.id}').data + assert res2.get('name_type') == BoardNameType.REALNAME + assert res2.get('created_by')['username'] == comment2.created_by.profile.realname def test_get_realname_comment_by_article_writer(self): res = self.http_request(self.user_with_kaist_info, 'get', f'comments/{self.realname_comment.id}').data @@ -455,7 +485,8 @@ def test_create_realname_subcomment(self): assert Comment.objects.get(content=comment_str).name_type == BoardNameType.REALNAME -@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', + 'set_user_with_kaist_info', 'set_user_without_kaist_info', 'set_boards', 'set_topics', 'set_articles', 'set_comments') class TestHiddenComments(TestCase, RequestSetting): def _comment_factory(self, **comment_kwargs): From c40655175afe6d89aeac4d381e1598d616b676b8 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 9 May 2022 23:06:27 +0900 Subject: [PATCH 071/101] Resolve conflicts --- tests/test_communication_article.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 7585f451..bec90f22 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -296,8 +296,7 @@ def test_PREPARING_ANSWER_to_BEFORE_SCHOOL_CONFIRM(self): assert self._get_communication_article_status(article) == status # 좋아요 수가 threshold를 넘었을 때 - users_tuple = (self.user2, self.user3, self.user4) - self._add_upvotes(article, users_tuple) + self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) assert self._get_communication_article_status(article) == status # 좋아요 수가 threshold 밑으로 내려갔다가 다시 올라갈 때 @@ -333,9 +332,8 @@ def test_days_left(self): # days_left가 설정되기 전 res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') assert res.data.get('days_left') == sys.maxsize - - user_tuple = (self.user2, self.user3, self.user4) - self._add_upvotes(article, user_tuple) + + self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') assert res.data.get('days_left') == ANSWER_PERIOD @@ -368,7 +366,7 @@ def test_descending_ordering_by_positive_vote_count(self): for vote_cnt, article in zip(vote_counts, articles): self._add_positive_votes(article, vote_cnt) - + res = self.http_request(self.user, 'get', 'articles', querystring='ordering=-positive_vote_count,-created_at') assert res.status_code == HTTP_200_OK From 0b22c9dd02cc4206875521fa5376c5ca7f71d60c Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Thu, 12 May 2022 23:05:49 +0900 Subject: [PATCH 072/101] Save communication article after creation --- apps/core/views/viewsets/article.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index 799d6e15..b35518db 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -142,9 +142,10 @@ def perform_create(self, serializer): instance = serializer.instance if Board.objects.get(pk=self.request.data['parent_board']).is_school_communication: - CommunicationArticle.objects.create( + communication_article = CommunicationArticle.objects.create( article=instance, ) + communication_article.save() def update(self, request, *args, **kwargs): article = self.get_object() From e34bc5aec50ea11617f7d67a38eccb8bb37c9e81 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Fri, 13 May 2022 01:21:27 +0900 Subject: [PATCH 073/101] Add flush_test command --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 2cdd285c..14048a9f 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ superuser: flush: mysql -u root -e 'DROP DATABASE new_ara;' +flush_test: + mysql -u root -e 'DROP DATABASE test_new_ara;' + reset: flush init run: From 098bd0ceea4d7e02e6539cfac5e6416fdfa1cedb Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Fri, 13 May 2022 01:23:50 +0900 Subject: [PATCH 074/101] Fix to update done status for only the first reply --- apps/core/models/signals/comment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/core/models/signals/comment.py b/apps/core/models/signals/comment.py index 13c4bc08..1cab8f9c 100644 --- a/apps/core/models/signals/comment.py +++ b/apps/core/models/signals/comment.py @@ -24,7 +24,8 @@ def update_article_commented_at(comment): def update_communication_article_status(comment): article = comment.parent_article if comment.parent_article else comment.parent_comment.parent_article if article.parent_board.is_school_communication and \ - comment.created_by.profile.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN: + comment.created_by.profile.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN and \ + article.communication_article.school_response_status != SchoolResponseStatus.ANSWER_DONE : article.communication_article.answered_at = timezone.now() article.communication_article.school_response_status = SchoolResponseStatus.ANSWER_DONE article.communication_article.save() From 51a511fe7e5c315f6d8d78dd72b52c2d55929493 Mon Sep 17 00:00:00 2001 From: TriangleYJ Date: Thu, 19 May 2022 18:05:00 +0900 Subject: [PATCH 075/101] Remove PREPARING_ANSWER, comfirend_by_school_at --- apps/core/admin.py | 2 - ...unicationarticle_confirmed_by_school_at.py | 17 ++++ apps/core/models/communication_article.py | 10 +-- .../core/serializers/communication_article.py | 8 +- .../views/viewsets/communication_article.py | 19 +---- tests/test_communication_article.py | 83 +------------------ 6 files changed, 25 insertions(+), 114 deletions(-) create mode 100644 apps/core/migrations/0041_remove_communicationarticle_confirmed_by_school_at.py diff --git a/apps/core/admin.py b/apps/core/admin.py index 5066f9f1..53d929ab 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -231,14 +231,12 @@ class ReportAdmin(MetaDataModelAdmin): class CommunicationArticleAdmin(MetaDataModelAdmin): list_filter = ( 'response_deadline', - 'confirmed_by_school_at', 'answered_at', ) list_display = ( 'article', 'get_status_string', 'response_deadline', - 'confirmed_by_school_at', 'answered_at', ) raw_id_fields = ( diff --git a/apps/core/migrations/0041_remove_communicationarticle_confirmed_by_school_at.py b/apps/core/migrations/0041_remove_communicationarticle_confirmed_by_school_at.py new file mode 100644 index 00000000..aaf8e9f7 --- /dev/null +++ b/apps/core/migrations/0041_remove_communicationarticle_confirmed_by_school_at.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.9 on 2022-05-12 16:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0040_alter_communication_article_field'), + ] + + operations = [ + migrations.RemoveField( + model_name='communicationarticle', + name='confirmed_by_school_at', + ), + ] diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index d9abb1d4..f7b792ed 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -10,8 +10,7 @@ class SchoolResponseStatus(IntEnum): BEFORE_UPVOTE_THRESHOLD = 0 BEFORE_SCHOOL_CONFIRM = 1 - PREPARING_ANSWER = 2 - ANSWER_DONE = 3 + ANSWER_DONE = 2 class CommunicationArticle(MetaDataModel): @@ -32,11 +31,6 @@ class Meta(MetaDataModel.Meta): default=timezone.datetime.min.replace(tzinfo=timezone.utc), verbose_name='답변 요청 기한', ) - - confirmed_by_school_at = models.DateTimeField( - default=timezone.datetime.min.replace(tzinfo=timezone.utc), - verbose_name='학교측 문의 확인 시각', - ) answered_at = models.DateTimeField( default=timezone.datetime.min.replace(tzinfo=timezone.utc), @@ -50,7 +44,7 @@ class Meta(MetaDataModel.Meta): @admin.display(description='진행 상황') def get_status_string(self) -> str: - status_list = ['소통 중', '답변 대기 중', '답변 준비 중', '답변 완료'] + status_list = ['소통 중', '답변 대기 중', '답변 완료'] return status_list[self.school_response_status] def __str__(self): diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py index 46da9ee3..0e5885ba 100644 --- a/apps/core/serializers/communication_article.py +++ b/apps/core/serializers/communication_article.py @@ -14,7 +14,6 @@ class Meta: read_only_fields = ( 'article', 'response_deadline', - 'answered_at', 'school_response_status', ) @@ -29,9 +28,4 @@ def get_days_left(obj) -> int: if obj.response_deadline == timezone.datetime.min.replace(tzinfo=timezone.utc): return sys.maxsize else: - return (obj.response_deadline.astimezone(timezone.localtime().tzinfo) - timezone.localtime()).days - - -class CommunicationArticleUpdateActionSerializer(BaseCommunicationArticleSerializer): - class Meta(BaseCommunicationArticleSerializer.Meta): - pass + return (obj.response_deadline.astimezone(timezone.localtime().tzinfo) - timezone.localtime()).days \ No newline at end of file diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py index 3de0cb99..0ffdcfee 100644 --- a/apps/core/views/viewsets/communication_article.py +++ b/apps/core/views/viewsets/communication_article.py @@ -5,15 +5,14 @@ from apps.core.permissions.communication_article import CommunicationArticleAdminPermission from ara.classes.viewset import ActionAPIViewSet -from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus -from apps.core.serializers.communication_article import CommunicationArticleUpdateActionSerializer, CommunicationArticleSerializer +from apps.core.models.communication_article import CommunicationArticle +from apps.core.serializers.communication_article import CommunicationArticleSerializer class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): queryset = CommunicationArticle.objects.all() serializer_class = CommunicationArticleSerializer action_serializer_class = { - 'update': CommunicationArticleUpdateActionSerializer, 'list': CommunicationArticleSerializer, } permission_classes = ( @@ -28,17 +27,3 @@ class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): # usage: /api/communication_articles/?school_response_status=1 filterset_fields = ['school_response_status'] - - # 학교 담당자가 신문고 게시글에 대해 `확인했습니다` 버튼을 누른 경우 - def update(self, request, *args, **kwargs): - # 이미 확인했습니다 단계를 지나간 경우 - if self.get_object().school_response_status >= SchoolResponseStatus.PREPARING_ANSWER: - return response.Response(status=status.HTTP_200_OK) - return super().update(request, *args, **kwargs) - - def perform_update(self, serializer): - serializer.save( - confirmed_by_school_at=timezone.now(), - school_response_status=SchoolResponseStatus.PREPARING_ANSWER, - ) - return super().perform_update(serializer) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index bec90f22..7e2d42f6 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -131,9 +131,6 @@ def _add_positive_votes(self, article, num): for user in dummy_users: self.http_request(user, 'post', f'articles/{article.id}/vote_positive') - def _confirm_communication_article(self, article, user): - return self.http_request(user, 'put', f'communication_articles/{article.id}') - def _add_admin_comment(self, article): comment_data = { 'content': 'Comment made in factory', @@ -161,8 +158,6 @@ def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_ pass elif status == SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM: self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) - elif status == SchoolResponseStatus.PREPARING_ANSWER: - self._confirm_communication_article(article, self.school_admin) elif status == SchoolResponseStatus.ANSWER_DONE: self._add_admin_comment(article) @@ -196,7 +191,6 @@ def test_create_communication_article(self): min_time = timezone.datetime.min.replace(tzinfo=timezone.utc) assert all([ communication_article.response_deadline == min_time, - communication_article.confirmed_by_school_at == min_time, communication_article.answered_at == min_time, communication_article.school_response_status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD ]) @@ -234,17 +228,6 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_BEFORE_SCHOOL_CONFIRM(self): assert self._get_communication_article_status(article) == to_status # 0 -> 2 - def test_BEFORE_UPVOTE_THRESHOLD_to_PREPARING_ANSWER(self): - from_status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD - to_status = SchoolResponseStatus.PREPARING_ANSWER - - article = self._create_article_with_status(from_status) - assert self._get_communication_article_status(article) == from_status - - self._confirm_communication_article(article, self.school_admin) - assert self._get_communication_article_status(article) == to_status - - # 0 -> 3 def test_BEFORE_UPVOTE_THRESHOLD_to_ANSWER_DONE(self): from_status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD to_status = SchoolResponseStatus.ANSWER_DONE @@ -256,17 +239,6 @@ def test_BEFORE_UPVOTE_THRESHOLD_to_ANSWER_DONE(self): assert self._get_communication_article_status(article) == to_status # 1 -> 2 - def test_BEFORE_SCHOOL_CONFIRM_to_PREPARING_ANSWER(self): - from_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM - to_status = SchoolResponseStatus.PREPARING_ANSWER - - article = self._create_article_with_status(from_status) - assert self._get_communication_article_status(article) == from_status - - self._confirm_communication_article(article, self.school_admin) - assert self._get_communication_article_status(article) == to_status - - # 1 -> 3 def test_BEFORE_SCHOOL_CONFIRM_to_ANSWER_DONE(self): from_status = SchoolResponseStatus.BEFORE_SCHOOL_CONFIRM to_status = SchoolResponseStatus.ANSWER_DONE @@ -277,35 +249,7 @@ def test_BEFORE_SCHOOL_CONFIRM_to_ANSWER_DONE(self): self._add_admin_comment(article) assert self._get_communication_article_status(article) == to_status - # 2 -> 3 - def test_PREPARING_ANSWER_to_ANSWER_DONE(self): - from_status = SchoolResponseStatus.PREPARING_ANSWER - to_status = SchoolResponseStatus.ANSWER_DONE - - article = self._create_article_with_status(from_status) - assert self._get_communication_article_status(article) == from_status - - self._add_admin_comment(article) - assert self._get_communication_article_status(article) == to_status - - # 2 -> 1 (관리자가 답변 준비 중일 때 좋아요 수가 다시 증가할 경우) - def test_PREPARING_ANSWER_to_BEFORE_SCHOOL_CONFIRM(self): - status = SchoolResponseStatus.PREPARING_ANSWER - - article = self._create_article_with_status(status) - assert self._get_communication_article_status(article) == status - - # 좋아요 수가 threshold를 넘었을 때 - self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) - assert self._get_communication_article_status(article) == status - - # 좋아요 수가 threshold 밑으로 내려갔다가 다시 올라갈 때 - # 정책상 소통게시판에서 vote_cancel은 허용되지 않으나, 테스트 정확성을 위해 첨부함 - self.http_request(self.user4, 'post', f'articles/{article.id}/vote_cancel') - self.http_request(self.user4, 'post', f'articles/{article.id}/vote_positive') - assert self._get_communication_article_status(article) == status - - # 3 -> 1 + # 2 -> 1 def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): status = SchoolResponseStatus.ANSWER_DONE @@ -315,16 +259,6 @@ def test_ANSWER_DONE_to_BEFORE_SCHOOL_CONFIRM(self): self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) assert self._get_communication_article_status(article) == status - # 3 -> 2 (관리자가 답변 완료한 글에 다시 확인했습니다 요청을 보낸 경우) - def test_ANSWER_DONE_to_PREPARING_ANSWER(self): - status = SchoolResponseStatus.ANSWER_DONE - - article = self._create_article_with_status(status) - assert self._get_communication_article_status(article) == status - - self._confirm_communication_article(article, self.school_admin) - assert self._get_communication_article_status(article) == status - def test_days_left(self): status = SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD article = self._create_article_with_status(status) @@ -412,14 +346,14 @@ def test_filtering_by_status(self): # 답변 완료(status=3) res = self.http_request(self.user, 'get', 'articles', - querystring='communication_article__school_response_status=3') + querystring=f'communication_article__school_response_status={SchoolResponseStatus.ANSWER_DONE}') assert res.status_code == HTTP_200_OK res_status_set = {el.get('communication_article_status') for el in res.data.get('results')} assert res_status_set == {SchoolResponseStatus.ANSWER_DONE} # 답변 전(status<3) res = self.http_request(self.user, 'get', 'articles', - querystring='communication_article__school_response_status__lt=3') + querystring=f'communication_article__school_response_status__lt={SchoolResponseStatus.ANSWER_DONE}') assert res.status_code == HTTP_200_OK res_status_set = {el.get('communication_article_status') for el in res.data.get('results')} assert res_status_set == {*SchoolResponseStatus} - {SchoolResponseStatus.ANSWER_DONE} @@ -466,13 +400,6 @@ def test_admin_can_view_admin_api_get(self): assert res.status_code == 200 assert res.data.get('article') == self.communication_article.article.id - def test_admin_can_update_admin_api(self): - res = self._confirm_communication_article(self.article, self.school_admin) - assert res.status_code == 200 - self.communication_article.refresh_from_db() - cas = CommunicationArticleSerializer(self.communication_article) - assert res.data.get('confirmed_by_school_at') == cas.data['confirmed_by_school_at'] - def test_user_cannot_view_admin_api_list(self): res = self.http_request(self.user, 'get', 'communication_articles') assert res.status_code == status.HTTP_403_FORBIDDEN @@ -480,7 +407,3 @@ def test_user_cannot_view_admin_api_list(self): def test_user_cannot_view_admin_api_get(self): res = self.http_request(self.user, 'get', f'communication_articles/{self.article.id}') assert res.status_code == status.HTTP_403_FORBIDDEN - - def test_user_cannot_update_admin_api(self): - res = self._confirm_communication_article(self.article, self.user) - assert res.status_code == status.HTTP_403_FORBIDDEN From aa05df5f57f3e30a85775ffb4739099ae1b7d590 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Thu, 26 May 2022 21:35:02 +0900 Subject: [PATCH 076/101] Add ban cancellation vote over threshold in realname article --- apps/core/views/viewsets/article.py | 5 ++++ tests/test_articles.py | 41 +++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index b35518db..c9df2467 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -9,6 +9,7 @@ from ara import redis from ara.classes.viewset import ActionAPIViewSet +from ara.settings import SCHOOL_RESPONSE_VOTE_THRESHOLD from apps.core.models import ( Article, @@ -213,6 +214,10 @@ def retrieve(self, request, *args, **kwargs): def vote_cancel(self, request, *args, **kwargs): article = self.get_object() + if article.name_type == BoardNameType.REALNAME and article.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: + return response.Response({'message': gettext('Cannot cancel vote on more than 30 votes in realname article')}, + status=status.HTTP_405_METHOD_NOT_ALLOWED) + if article.is_hidden_by_reported(): return response.Response({'message': gettext('Cannot cancel vote on articles hidden by reports')}, status=status.HTTP_403_FORBIDDEN) diff --git a/tests/test_articles.py b/tests/test_articles.py index 4c1304b0..7dc44981 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -6,6 +6,7 @@ from apps.core.models import Article, Topic, Board, Block, Vote, Comment from apps.core.models.board import BoardNameType from apps.user.models import UserProfile +from ara.settings import SCHOOL_RESPONSE_VOTE_THRESHOLD from tests.conftest import RequestSetting, TestCase @@ -80,6 +81,25 @@ def set_articles(request): commented_at=timezone.now() ) +@pytest.fixture(scope='class') +def set_realname_article(request): + """set_user_with_kaist_info,, set_realname_topic, set_realname_board 먼저 적용""" + request.cls.realname_article = Article.objects.create( + title='Realname Test Article', + content='Content of test realname article', + content_text='Content of test article in text', + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=request.cls.user_with_kaist_info, + parent_topic=request.cls.realname_topic, + parent_board=request.cls.realname_board, + commented_at=timezone.now() + ) + @pytest.fixture(scope='function') def set_kaist_articles(request): @@ -463,8 +483,8 @@ def test_deleting_with_comments(self): ).count() == 0 assert self.article.comment_count == 0 -@pytest.mark.usefixtures('set_user_client', 'set_user_with_kaist_info', 'set_user_without_kaist_info', - 'set_boards', 'set_topics', 'set_articles') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', 'set_user_without_kaist_info', + 'set_boards', 'set_topics', 'set_articles', 'set_realname_article') class TestRealnameArticle(TestCase, RequestSetting): def test_get_realname_article(self): # kaist info가 있는 유저가 생성한 게시글 @@ -554,6 +574,23 @@ def test_update_realname_article(self): assert result.get('name_type') == BoardNameType.REALNAME assert Article.objects.get(title=new_title).name_type == BoardNameType.REALNAME + def test_ban_canclellation_vote_after_30(self): + # SCHOOL_RESPONSE_VOTE_THRESHOLD is 3 in test + users = [self.user, self.user2] + for user in users: + self.http_request(user, 'post', f'articles/{self.realname_article.id}/vote_positive') + + res1 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_positive') + article = Article.objects.get(id=self.realname_article.id) + assert res1.status_code == 200 + assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD + + res2 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_cancel') + article = Article.objects.get(id=self.realname_article.id) + assert res2.status_code == 405 + assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD + + @pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', 'set_user_without_kaist_info', 'set_boards', 'set_topics', 'set_articles') class TestHiddenArticles(TestCase, RequestSetting): From a0609f56756826b104ad52cea34089daba30e3ae Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Fri, 13 May 2022 00:45:55 +0900 Subject: [PATCH 077/101] Add read & write access masks --- .../0041_add_read_and_write_access_masks.py | 27 +++++++++++++++++++ apps/core/models/board.py | 16 ++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 apps/core/migrations/0041_add_read_and_write_access_masks.py diff --git a/apps/core/migrations/0041_add_read_and_write_access_masks.py b/apps/core/migrations/0041_add_read_and_write_access_masks.py new file mode 100644 index 00000000..29b42af3 --- /dev/null +++ b/apps/core/migrations/0041_add_read_and_write_access_masks.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.9 on 2022-05-12 15:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0040_alter_communication_article_field'), + ] + + operations = [ + migrations.RemoveField( + model_name='board', + name='access_mask', + ), + migrations.AddField( + model_name='board', + name='read_access_mask', + field=models.SmallIntegerField(default=222, verbose_name='읽기 권한'), + ), + migrations.AddField( + model_name='board', + name='write_access_mask', + field=models.SmallIntegerField(default=218, verbose_name='쓰기 권한'), + ), + ] diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 51258a0d..3804651f 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -41,13 +41,21 @@ class Meta(MetaDataModel.Meta): ) # 사용자 그룹에 대해 접근 권한을 제어하는 bit mask 입니다. - # access_mask & (1< 0 일 때 접근이 가능합니다. + # access_mask & (1 << user.group) > 0 일 때 접근이 가능합니다. # 사용자 그룹의 값들은 `UserGroup`을 참고하세요. - access_mask = models.IntegerField( - default=int('11011110', 2), # 카이스트 구성원만 사용 가능 + read_access_mask = models.SmallIntegerField( + # UNAUTHORIZED, EXTERNAL_ORG 제외 모든 사용자 읽기 권한 부여 + default=0b11011110, null=False, - verbose_name='접근 권한 값' + verbose_name='읽기 권한' ) + write_access_mask = models.SmallIntegerField( + # UNAUTHORIZED, STORE_EMPLOYEE, EXTERNAL_ORG 제외 모든 사용자 쓰기 권한 부여 + default=0b11011010, + null=False, + verbose_name='쓰기 권한' + ) + is_readonly = models.BooleanField( verbose_name='읽기 전용 게시판', help_text='활성화했을 때 관리자만 글을 쓸 수 있습니다. (ex. 포탈공지)', From 878247e78c7f318cfa311dce8b202c923b3e39c6 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Fri, 13 May 2022 01:07:29 +0900 Subject: [PATCH 078/101] Update group_has_access method --- apps/core/models/board.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 3804651f..944a36c3 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -117,5 +117,8 @@ class Meta(MetaDataModel.Meta): def __str__(self) -> str: return self.ko_name - def group_has_access(self, group: int) -> bool: - return (self.access_mask & (1 << group)) > 0 + def group_has_read_access(self, group: int) -> bool: + return (self.read_access_mask & (1 << group)) > 0 + + def group_has_write_access(self, group: int) -> bool: + return (self.write_access_mask & (1 << group)) > 0 From efe74bf5905c384e4e27c68309b1a3a32d349b21 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 23 May 2022 23:44:40 +0900 Subject: [PATCH 079/101] Update group_has_access to read/write access --- apps/core/models/article.py | 2 +- apps/core/serializers/article.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 55c2c0fc..9502d566 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -262,7 +262,7 @@ def hidden_reasons(self, user: settings.AUTH_USER_MODEL) -> typing.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(user.profile.group): + if not self.parent_board.group_has_read_access(user.profile.group): reasons.append(ArticleHiddenReason.ACCESS_DENIED_CONTENT) return reasons diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 1ef4350b..6139096b 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -421,11 +421,11 @@ class Meta(BaseArticleSerializer.Meta): def validate_parent_board(self, board: Board): user_is_superuser = self.context['request'].user.is_superuser - user_has_perm = board.group_has_access(self.context['request'].user.profile.group) if not user_is_superuser and board.is_readonly: raise serializers.ValidationError(gettext('This board is read only.')) - if not user_has_perm: - raise serializers.ValidationError(gettext('This board is only for KAIST members.')) + user_has_read_perm = board.group_has_read_access(self.context['request'].user.profile.group) + if not user_has_read_perm: + raise serializers.ValidationError(gettext('Read permission denied')) return board From 5c4635ff4ba6e30fc90775bed93973885b58f052 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 23 May 2022 23:54:35 +0900 Subject: [PATCH 080/101] Check access mask when creating article & comment --- apps/core/views/viewsets/article.py | 8 ++++++++ apps/core/views/viewsets/comment.py | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index b35518db..c07397f0 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -133,6 +133,14 @@ def filter_queryset(self, queryset): ) return queryset + + def create(self, request, *args, **kwargs): + user_group = request.user.profile.group + board = Board.objects.get(pk=self.request.data['parent_board']) + if board.group_has_write_access(user_group): + return super().create(request, *args, **kwargs) + return response.Response({'message': gettext('Permission denied')}, + status=status.HTTP_403_FORBIDDEN) def perform_create(self, serializer): serializer.save( diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 52d15ba5..b999c2a1 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -55,7 +55,12 @@ class CommentViewSet(mixins.CreateModelMixin, } def create(self, request, *args, **kwargs): - return super().create(request, *args, **kwargs) + user_group = request.user.profile.group + parent_article = Article.objects.get(pk=self.request.data['parent_article']) + if parent_article.parent_board.group_has_write_access(user_group): + return super().create(request, *args, **kwargs) + return response.Response({'message': gettext('Permission denied')}, + status=status.HTTP_403_FORBIDDEN) def perform_create(self, serializer): parent_article_id = self.request.data.get('parent_article') From 11b739b49e0722f6065d578fd07da4a924491447 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Mon, 23 May 2022 23:57:22 +0900 Subject: [PATCH 081/101] Test check write permission when creating article --- tests/test_articles.py | 103 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/tests/test_articles.py b/tests/test_articles.py index 4c1304b0..484ad5ed 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -1,6 +1,7 @@ import pytest from django.contrib.auth.models import User from django.utils import timezone +from rest_framework import status from rest_framework.test import APIClient from apps.core.models import Article, Topic, Board, Block, Vote, Comment @@ -38,6 +39,62 @@ def set_boards(request): name_type=BoardNameType.REALNAME, ) + request.cls.regular_access_board = Board.objects.create( + slug='regular access', + ko_name='일반 접근 권한 게시판', + en_name='Regular Access Board', + ko_description='일반 접근 권한 게시판', + en_description='Regular Access Board', + read_access_mask=0b11011110, + write_access_mask=0b11011010 + ) + + request.cls.advertiser_readable_board = Board.objects.create( + slug='advertiser readable', + ko_name='외부인(홍보 계정) 열람 가능 게시판', + en_name='Advertiser Readable Board', + ko_description='외부인(홍보 계정) 열람 가능 게시판', + en_description='Advertiser Readable Board', + read_access_mask=0b11111110 + ) + + request.cls.nonwritable_board = Board.objects.create( + slug='nonwritable', + ko_name='글 작성 불가 게시판', + en_name='Nonwritable Board', + ko_description='글 작성 불가 게시판', + en_description='Nonwritable Board', + write_access_mask=0b00000000 + ) + + request.cls.newsadmin_writable_board = Board.objects.create( + slug='newsadmin writable', + ko_name='뉴스게시판 관리인 글 작성 가능 게시판', + en_name='Newsadmin Writable Board', + ko_description='뉴스게시판 관리인 글 작성 가능 게시판', + en_description='Newsadmin Writable Board', + write_access_mask=0b10000000 + ) + + request.cls.enterprise_writable_board = Board.objects.create( + slug='enterprise writable', + ko_name='입주업체 글 작성 가능 게시판', + en_name='Enterprise Writable Board', + ko_description='입주업체 글 작성 가능 게시판', + en_description='Enterprise Writable Board', + write_access_mask=0b11011110 + ) + + # Though its name is 'advertiser writable', enterprise can also write to it + request.cls.advertiser_writable_board = Board.objects.create( + slug='advertiser writable', + ko_name='외부인(홍보 계정) 글 작성 가능 게시판', + en_name='Advertiser Writable Board', + ko_description='외부인(홍보 계정) 글 작성 가능 게시판', + en_description='Advertiser Writable Board', + write_access_mask=0b11111110 + ) + @pytest.fixture(scope='class') def set_topics(request): @@ -135,6 +192,25 @@ def set_readonly_board(request): @pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_client3', 'set_user_client4', 'set_user_with_kaist_info', 'set_boards', 'set_topics', 'set_articles') class TestArticle(TestCase, RequestSetting): + def _create_user_by_group(self, group): + user, created = User.objects.get_or_create( + username=f'User in group {group}', + email=f'group{group}user@sparcs.org' + ) + if created: + UserProfile.objects.create( + user=user, + nickname=f'Nickname in group {group}', + group=group, + agree_terms_of_service_at=timezone.now(), + sso_user_info={ + 'kaist_info': '{\"ku_kname\": \"\\ud669\"}', + 'first_name': f'Group{group}User_FirstName', + 'last_name': f'Group{group}User_LastName' + } + ) + return user + def test_list(self): # article 개수를 확인하는 테스트 res = self.http_request(self.user, 'get', 'articles') @@ -233,7 +309,32 @@ def test_create(self): # convert user data to JSON self.http_request(self.user, 'post', 'articles', user_data) assert Article.objects.filter(title='article for test_create') - + + # create 단계에서 user의 write 권한 확인 테스트 + def test_check_write_permission_when_create(self): + group_users = [self._create_user_by_group(group) for group in UserProfile.UserGroup] + boards = [ + self.regular_access_board, + self.nonwritable_board, + self.newsadmin_writable_board, + self.enterprise_writable_board, + self.advertiser_writable_board + ] + + for user in group_users: + for board in boards: + res = self.http_request(user, 'post', 'articles', { + 'title': 'title in write permission test', + 'content': 'content in write permission test', + 'content_text': 'content_text in write permission test', + 'parent_board': board.id + }) + + if board.group_has_write_access(user.profile.group): + assert res.status_code == status.HTTP_201_CREATED + else: + assert res.status_code == status.HTTP_403_FORBIDDEN + def test_create_anonymous(self): user_data = { "title": "article for test_create", From 27535a756f5895a4ae86238c2e03c6dd22a0a40a Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Tue, 24 May 2022 00:29:09 +0900 Subject: [PATCH 082/101] Test check read permission when getting an article --- tests/test_articles.py | 60 +++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/tests/test_articles.py b/tests/test_articles.py index 484ad5ed..4c1223a8 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -122,19 +122,35 @@ def set_topics(request): def set_articles(request): """set_board 먼저 적용""" request.cls.article = Article.objects.create( - title="example article", - content="example content", - content_text="example content text", - name_type=BoardNameType.REGULAR, - is_content_sexual=False, - is_content_social=False, - hit_count=0, - positive_vote_count=0, - negative_vote_count=0, - created_by=request.cls.user, - parent_topic=request.cls.topic, - parent_board=request.cls.board, - commented_at=timezone.now() + title="example article", + content="example content", + content_text="example content text", + name_type=BoardNameType.REGULAR, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=request.cls.user, + parent_topic=request.cls.topic, + parent_board=request.cls.board, + commented_at=timezone.now() + ) + + request.cls.regular_access_article = Article.objects.create( + title='regular access article', + content='regular access article content', + content_text='regular access article content', + created_by=request.cls.user, + parent_board=request.cls.regular_access_board + ) + + request.cls.advertiser_readable_article = Article.objects.create( + title='advertiser readable article', + content='advertiser readable article content', + content_text='advertiser readable article content', + created_by=request.cls.user, + parent_board=request.cls.advertiser_readable_board ) @@ -310,6 +326,24 @@ def test_create(self): self.http_request(self.user, 'post', 'articles', user_data) assert Article.objects.filter(title='article for test_create') + # get request 시 user의 read 권한 확인 테스트 + def test_check_read_permission_when_get(self): + group_users = [self._create_user_by_group(group) for group in UserProfile.UserGroup] + articles = [ + self.regular_access_article, + self.advertiser_readable_article + ] + + for user in group_users: + for article in articles: + res = self.http_request(user, 'get', f'articles/{article.id}') + + if article.parent_board.group_has_read_access(user.profile.group): + assert res.status_code == status.HTTP_200_OK + assert res.data['id'] == article.id + else: + assert res.status_code == status.HTTP_403_FORBIDDEN + # create 단계에서 user의 write 권한 확인 테스트 def test_check_write_permission_when_create(self): group_users = [self._create_user_by_group(group) for group in UserProfile.UserGroup] From d1699dbd6191f08b1bbe9c71b28efd31174c3853 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 25 May 2022 01:14:34 +0900 Subject: [PATCH 083/101] Add user_readable/writable fields to serializer --- apps/core/serializers/board.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/core/serializers/board.py b/apps/core/serializers/board.py index 9e7336f7..3ec11edc 100644 --- a/apps/core/serializers/board.py +++ b/apps/core/serializers/board.py @@ -1,5 +1,6 @@ -from ara.classes.serializers import MetaDataModelSerializer +from rest_framework import serializers +from ara.classes.serializers import MetaDataModelSerializer from apps.core.models import Board @@ -20,3 +21,13 @@ class BoardDetailActionSerializer(BaseBoardSerializer): read_only=True, source='topic_set', ) + user_readable = serializers.SerializerMethodField() + user_writable = serializers.SerializerMethodField() + + def get_user_readable(self, obj): + user = self.context['request'].user + return obj.group_has_read_access(user.profile.group) + + def get_user_writable(self, obj): + user = self.context['request'].user + return obj.group_has_write_access(user.profile.group) From e2e5e452cf994f2455c49a7cefdbaeeb7e2ff572 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 26 May 2022 21:09:51 +0900 Subject: [PATCH 084/101] Add article read/write permissions --- apps/core/permissions/article.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/core/permissions/article.py b/apps/core/permissions/article.py index 748845c5..6873dfa0 100644 --- a/apps/core/permissions/article.py +++ b/apps/core/permissions/article.py @@ -16,4 +16,19 @@ class ArticleKAISTPermission(permissions.BasePermission): message = 'KAIST 구성원만 읽을 수 있는 게시물입니다.' def has_object_permission(self, request, view, obj: Article): - return obj.parent_board.group_has_access(request.user.profile.group) + print('I am in retrieve in has_object_permission') + return obj.parent_board.group_has_read_access(request.user.profile.group) + + +class ArticleReadPermission(permissions.BasePermission): + message = '해당 게시물에 대한 읽기 권한이 없습니다.' + + def has_object_permission(self, request, view, obj: Article): + return obj.parent_board.group_has_read_access(request.user.profile.group) + + +class ArticleWritePermission(permissions.BasePermission): + message = '해당 게시물에 대한 쓰기 권한이 없습니다.' + + def has_object_permission(self, request, view, obj: Article): + return obj.parent_board.group_has_write_access(request.user.profile.group) From ee877617dd2f55c25e1209fba955d114b0a7d418 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 26 May 2022 21:12:08 +0900 Subject: [PATCH 085/101] Add permission check in comment creation step --- apps/core/views/viewsets/comment.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index b999c2a1..e6d15109 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -1,4 +1,6 @@ from django.conf import settings +from django.utils import timezone +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext from rest_framework import mixins, status, response, decorators, serializers, permissions @@ -55,8 +57,14 @@ class CommentViewSet(mixins.CreateModelMixin, } def create(self, request, *args, **kwargs): + if self.request.data['parent_article'] is None: + parent_article = Comment.objects.get(pk=self.request.data['parent_comment']).parent_article + else: + article_queryset = Article.objects.queryset_with_deleted + parent_article = get_object_or_404(article_queryset, pk=self.request.data['parent_article']) + if parent_article.deleted_at != timezone.datetime.min.replace(tzinfo=timezone.utc): + return response.Response(status=status.HTTP_410_GONE) user_group = request.user.profile.group - parent_article = Article.objects.get(pk=self.request.data['parent_article']) if parent_article.parent_board.group_has_write_access(user_group): return super().create(request, *args, **kwargs) return response.Response({'message': gettext('Permission denied')}, From 2d319af4f8d6412d2956d7c8104c84b42777354e Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 26 May 2022 21:12:44 +0900 Subject: [PATCH 086/101] Add permission checking in article & comment tests --- tests/test_articles.py | 112 +++++++++++++++++++---------------------- tests/test_comments.py | 55 ++++++++++++++++---- 2 files changed, 97 insertions(+), 70 deletions(-) diff --git a/tests/test_articles.py b/tests/test_articles.py index 4c1223a8..e3ecbd93 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -49,13 +49,15 @@ def set_boards(request): write_access_mask=0b11011010 ) - request.cls.advertiser_readable_board = Board.objects.create( - slug='advertiser readable', - ko_name='외부인(홍보 계정) 열람 가능 게시판', - en_name='Advertiser Readable Board', - ko_description='외부인(홍보 계정) 열람 가능 게시판', - en_description='Advertiser Readable Board', - read_access_mask=0b11111110 + # Though its name is 'advertiser accessible', enterprise is also accessible + request.cls.advertiser_accessible_board = Board.objects.create( + slug='advertiser accessible', + ko_name='외부인(홍보 계정) 접근 가능 게시판', + en_name='Advertiser Accessible Board', + ko_description='외부인(홍보 계정) 접근 가능 게시판', + en_description='Advertiser Accessible Board', + read_access_mask=0b11111110, + write_access_mask=0b11111110 ) request.cls.nonwritable_board = Board.objects.create( @@ -85,16 +87,6 @@ def set_boards(request): write_access_mask=0b11011110 ) - # Though its name is 'advertiser writable', enterprise can also write to it - request.cls.advertiser_writable_board = Board.objects.create( - slug='advertiser writable', - ko_name='외부인(홍보 계정) 글 작성 가능 게시판', - en_name='Advertiser Writable Board', - ko_description='외부인(홍보 계정) 글 작성 가능 게시판', - en_description='Advertiser Writable Board', - write_access_mask=0b11111110 - ) - @pytest.fixture(scope='class') def set_topics(request): @@ -145,24 +137,38 @@ def set_articles(request): parent_board=request.cls.regular_access_board ) - request.cls.advertiser_readable_article = Article.objects.create( + request.cls.advertiser_accessible_article = Article.objects.create( title='advertiser readable article', content='advertiser readable article content', content_text='advertiser readable article content', created_by=request.cls.user, - parent_board=request.cls.advertiser_readable_board + parent_board=request.cls.advertiser_accessible_board ) @pytest.fixture(scope='function') def set_kaist_articles(request): - request.cls.non_kaist_user, _ = User.objects.get_or_create(username='NonKaistUser', email='non-kaist-user@sparcs.org') + request.cls.non_kaist_user, _ = User.objects.get_or_create( + username='NonKaistUser', + email='non-kaist-user@sparcs.org' + ) if not hasattr(request.cls.non_kaist_user, 'profile'): - UserProfile.objects.get_or_create(user=request.cls.non_kaist_user, nickname='Not a KAIST User', agree_terms_of_service_at=timezone.now()) - request.cls.kaist_user, _ = User.objects.get_or_create(username='KaistUser', email='kaist-user@sparcs.org') + UserProfile.objects.get_or_create( + user=request.cls.non_kaist_user, + nickname='Not a KAIST User', + agree_terms_of_service_at=timezone.now() + ) + request.cls.kaist_user, _ = User.objects.get_or_create( + username='KaistUser', + email='kaist-user@sparcs.org' + ) if not hasattr(request.cls.kaist_user, 'profile'): - UserProfile.objects.get_or_create(user=request.cls.kaist_user, nickname='KAIST User', - group=UserProfile.UserGroup.KAIST_MEMBER, agree_terms_of_service_at=timezone.now()) + UserProfile.objects.get_or_create( + user=request.cls.kaist_user, + nickname='KAIST User', + agree_terms_of_service_at=timezone.now(), + group=UserProfile.UserGroup.KAIST_MEMBER + ) request.cls.kaist_board, _ = Board.objects.get_or_create( slug="kaist-only", @@ -170,7 +176,8 @@ def set_kaist_articles(request): en_name="KAIST Board", ko_description="KAIST Board", en_description="KAIST Board", - access_mask=2 + read_access_mask=0b00000010, + write_access_mask=0b00000010 ) request.cls.kaist_article, _ = Article.objects.get_or_create( title="example article", @@ -230,7 +237,7 @@ def _create_user_by_group(self, group): def test_list(self): # article 개수를 확인하는 테스트 res = self.http_request(self.user, 'get', 'articles') - assert res.data.get('num_items') == 1 + assert res.data.get('num_items') == Article.objects.count() Article.objects.create( title="example article", @@ -265,7 +272,7 @@ def test_list(self): ) res = self.http_request(self.user, 'get', 'articles') - assert res.data.get('num_items') == 3 + assert res.data.get('num_items') == Article.objects.count() def test_get(self): # article 조회가 잘 되는지 확인 @@ -331,7 +338,7 @@ def test_check_read_permission_when_get(self): group_users = [self._create_user_by_group(group) for group in UserProfile.UserGroup] articles = [ self.regular_access_article, - self.advertiser_readable_article + self.advertiser_accessible_article ] for user in group_users: @@ -352,7 +359,7 @@ def test_check_write_permission_when_create(self): self.nonwritable_board, self.newsadmin_writable_board, self.enterprise_writable_board, - self.advertiser_writable_board + self.advertiser_accessible_board ] for user in group_users: @@ -421,11 +428,18 @@ def test_update_cache_sync(self): assert response.data.get('title') == new_title assert response.data.get('content') == new_content + @pytest.mark.usefixtures('set_kaist_articles') def test_update_hit_counts(self): - previous_hit_count = self.article.hit_count + updated_hit_count = self.article.hit_count + 1 + res = self.http_request(self.user2, 'get', f'articles/{self.article.id}').data + assert res.get('hit_count') == updated_hit_count + assert Article.objects.get(id=self.article.id).hit_count == updated_hit_count + + # 권한 없는 사용자가 get + self.http_request(self.non_kaist_user, 'get', f'articles/{self.article.id}') + res = self.http_request(self.user2, 'get', f'articles/{self.article.id}').data - assert res.get('hit_count') == previous_hit_count + 1 - assert Article.objects.get(id=self.article.id).hit_count == previous_hit_count + 1 + assert res.get('hit_count') == updated_hit_count def test_delete_by_non_writer(self): # 글쓴이가 아닌 사람은 글을 지울 수 없음 @@ -522,7 +536,7 @@ def test_self_vote(self): def test_kaist_permission(self): # 카이스트 구성원만 볼 수 있는 게시판에 대한 테스트 def check_kaist_error(response): - assert response.status_code == 403 + assert response.status_code == status.HTTP_403_FORBIDDEN assert 'KAIST' in response.data['detail'] # 에러 메세지 체크 # 게시물 읽기 테스트 check_kaist_error(self.http_request(self.non_kaist_user, 'get', f'articles/{self.kaist_article.id}')) @@ -558,20 +572,22 @@ def test_read_status(self): assert res1.data['results'][0]['read_status'] == 'N' assert res2.data['results'][0]['read_status'] == 'N' + article_id = res1.data['results'][0]['id'] + # user2만 읽음 - self.http_request(self.user2, 'get', f'articles/{self.article.id}') + self.http_request(self.user2, 'get', f'articles/{article_id}') res1 = self.http_request(self.user, 'get', 'articles') res2 = self.http_request(self.user2, 'get', 'articles') assert res1.data['results'][0]['read_status'] == 'N' assert res2.data['results'][0]['read_status'] == '-' # user1이 업데이트 (user2은 아직 변경사항 확인못함) - self.http_request(self.user, 'get', f'articles/{self.article.id}') - self.http_request(self.user, 'patch', f'articles/{self.article.id}', {'content': 'update!'}) + self.http_request(self.user, 'get', f'articles/{article_id}') + self.http_request(self.user, 'patch', f'articles/{article_id}', {'content': 'update!'}) # TODO: 현재는 프론트 구현상 게시물을 수정하면 바로 다시 GET을 호출하기 때문에 '-' 로 나옴. # 추후 websocket 등으로 게시물 수정이 실시간으로 이루어진다면, 'U'로 나오기 때문에 수정 필요. - self.http_request(self.user, 'get', f'articles/{self.article.id}') + self.http_request(self.user, 'get', f'articles/{article_id}') res1 = self.http_request(self.user, 'get', 'articles') assert res1.data['results'][0]['read_status'] == '-' @@ -974,25 +990,3 @@ def test_report_already_hidden_article(self): }) assert res.status_code == 403 - - def test_comment_on_deleted_article(self): - target_article = self._create_deleted_article() - - res = self.http_request(self.user, 'post', 'comments', { - 'content': 'This is a comment', - 'parent_article': target_article.id, - 'name_type': BoardNameType.REGULAR, - }) - - assert res.status_code == 400 - - def test_comment_on_report_hidden_article(self): - target_article = self._create_report_hidden_article() - - res = self.http_request(self.user, 'post', 'comments', { - 'content': 'This is a comment', - 'parent_article': target_article.id, - 'name_type': BoardNameType.REGULAR, - }) - - assert res.status_code == 201 diff --git a/tests/test_comments.py b/tests/test_comments.py index e8358abc..0c205551 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import User from django.utils import timezone from django.utils.translation import gettext +from rest_framework import status from rest_framework.test import APIClient from apps.core.models import Article, Topic, Board, Comment, Block, Vote @@ -91,7 +92,6 @@ def set_articles(request): commented_at=timezone.now() ) - """set_realname_topic, set_user_with_kaist_info 먼저 적용""" request.cls.realname_article = Article.objects.create( title='Realname Test Article', @@ -109,7 +109,6 @@ def set_articles(request): commented_at=timezone.now() ) - """set_realname_topic, set_user_without_kaist_info 먼저 적용""" request.cls.realname_article_without_kinfo = Article.objects.create( title='Realname Test Article', @@ -338,16 +337,16 @@ def test_anonymous_comment_by_article_writer(self): assert article_writer_id2 == comment_writer_id2 def test_comment_on_regular_parent_article(self): - comment_str = 'This is a comment on a regular parent article' comment_data = { - 'content': comment_str, + 'content': 'This is a comment on a regular parent article', 'parent_article': self.article_regular.id, 'parent_comment': None, 'attachment': None, } - self.http_request(self.user, 'post', 'comments', comment_data) - Comment.objects.filter(content=comment_str).first() - assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.REGULAR + res = self.http_request(self.user, 'post', 'comments', comment_data) + + assert res.data['name_type'] == BoardNameType.REGULAR + assert Comment.objects.get(pk=res.data['id']).name_type == BoardNameType.REGULAR def test_comment_on_regular_parent_comment(self): comment_str = 'This is a comment on a regular parent comment' @@ -372,15 +371,49 @@ def test_comment_on_anonymous_parent_article(self): assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS def test_comment_on_anonymous_parent_comment(self): - comment_str = 'This is a comment on an anonymous parent comment' comment_data = { - 'content': comment_str, + 'content': 'This is a comment on an anonymous parent comment', 'parent_article': None, 'parent_comment': self.comment_anonymous.id, 'attachment': None, } - self.http_request(self.user, 'post', 'comments', comment_data) - assert Comment.objects.filter(content=comment_str).first().name_type == BoardNameType.ANONYMOUS + res = self.http_request(self.user, 'post', 'comments', comment_data) + + assert res.data['name_type'] == BoardNameType.ANONYMOUS + assert Comment.objects.get(pk=res.data['id']).name_type == BoardNameType.ANONYMOUS + + def test_comment_on_deleted_article(self): + deleted_article = Article.objects.create( + title='deleted article', + content='deleted article content', + content_text='deleted article content text', + created_by=self.user, + parent_board=self.board, + deleted_at=timezone.now() + ) + res = self.http_request(self.user, 'post', 'comments', { + 'content': 'deleted article comment content', + 'parent_article': deleted_article.id, + 'name_type': BoardNameType.REGULAR + }) + assert res.status_code == status.HTTP_410_GONE + + def test_comment_on_report_hidden_article(self): + report_hidden_article = Article.objects.create( + title='report hidden article', + content='report hidden article content', + content_text='report hidden article content text', + created_by=self.user, + parent_board=self.board, + report_count=1_000_000, + hidden_at=timezone.now() + ) + res = self.http_request(self.user, 'post', 'comments', { + 'content': 'report hidden article comment content', + 'parent_article': report_hidden_article.id, + 'name_type': BoardNameType.REGULAR + }) + assert res.status_code == status.HTTP_201_CREATED # 댓글 좋아요 확인 def test_positive_vote(self): From 8c76005c7e60984b0fa72611e30bb349dda34aa9 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 26 May 2022 21:43:55 +0900 Subject: [PATCH 087/101] Reformat Meta data in ArticleSerializer --- apps/core/serializers/article.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 6139096b..c15d4876 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -119,7 +119,11 @@ class Meta(BaseArticleSerializer.Meta): class ArticleSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): class Meta(BaseArticleSerializer.Meta): exclude = ( - 'migrated_hit_count', 'migrated_positive_vote_count', 'migrated_negative_vote_count', 'content_text',) + 'migrated_hit_count', + 'migrated_positive_vote_count', + 'migrated_negative_vote_count', + 'content_text' + ) @staticmethod def search_articles(queryset, search): From 9a70317ba8e33b9c3ce842ade6a41a74293e1506 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 26 May 2022 21:54:27 +0900 Subject: [PATCH 088/101] Add migration file --- ...te_access_masks.py => 0042_add_read_write_access_masks.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apps/core/migrations/{0041_add_read_and_write_access_masks.py => 0042_add_read_write_access_masks.py} (83%) diff --git a/apps/core/migrations/0041_add_read_and_write_access_masks.py b/apps/core/migrations/0042_add_read_write_access_masks.py similarity index 83% rename from apps/core/migrations/0041_add_read_and_write_access_masks.py rename to apps/core/migrations/0042_add_read_write_access_masks.py index 29b42af3..92fc69ca 100644 --- a/apps/core/migrations/0041_add_read_and_write_access_masks.py +++ b/apps/core/migrations/0042_add_read_write_access_masks.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.9 on 2022-05-12 15:39 +# Generated by Django 3.2.9 on 2022-05-26 12:53 from django.db import migrations, models @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0040_alter_communication_article_field'), + ('core', '0041_remove_communicationarticle_confirmed_by_school_at'), ] operations = [ From ccd75a58d88419713f04f891b2561af20f629615 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Fri, 27 May 2022 01:29:56 +0900 Subject: [PATCH 089/101] Fix typo, condition order in vote_cancel --- apps/core/views/viewsets/article.py | 8 ++++---- tests/test_articles.py | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index c9df2467..c89631da 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -214,14 +214,14 @@ def retrieve(self, request, *args, **kwargs): def vote_cancel(self, request, *args, **kwargs): article = self.get_object() - if article.name_type == BoardNameType.REALNAME and article.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: - return response.Response({'message': gettext('Cannot cancel vote on more than 30 votes in realname article')}, - status=status.HTTP_405_METHOD_NOT_ALLOWED) - if article.is_hidden_by_reported(): return response.Response({'message': gettext('Cannot cancel vote on articles hidden by reports')}, status=status.HTTP_403_FORBIDDEN) + if article.name_type == BoardNameType.REALNAME and article.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: + return response.Response({'message': gettext('It is not available to cancel a vote for a real name article with more than 30 votes.')}, + status=status.HTTP_405_METHOD_NOT_ALLOWED) + if not Vote.objects.filter( voted_by=request.user, parent_article=article, diff --git a/tests/test_articles.py b/tests/test_articles.py index 7dc44981..ce9fafa9 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import User from django.utils import timezone from rest_framework.test import APIClient +from rest_framework import status from apps.core.models import Article, Topic, Board, Block, Vote, Comment from apps.core.models.board import BoardNameType @@ -574,7 +575,7 @@ def test_update_realname_article(self): assert result.get('name_type') == BoardNameType.REALNAME assert Article.objects.get(title=new_title).name_type == BoardNameType.REALNAME - def test_ban_canclellation_vote_after_30(self): + def test_ban_vote_canclellation_after_30(self): # SCHOOL_RESPONSE_VOTE_THRESHOLD is 3 in test users = [self.user, self.user2] for user in users: @@ -582,12 +583,12 @@ def test_ban_canclellation_vote_after_30(self): res1 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_positive') article = Article.objects.get(id=self.realname_article.id) - assert res1.status_code == 200 + assert res1.status_code == status.HTTP_200_OK assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD res2 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_cancel') article = Article.objects.get(id=self.realname_article.id) - assert res2.status_code == 405 + assert res2.status_code == status.HTTP_405_METHOD_NOT_ALLOWED assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD From a844010bb11de0d8c0c578dba89953861d365ca6 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 2 Jun 2022 03:12:30 +0900 Subject: [PATCH 090/101] Update permission checking codes --- apps/core/permissions/article.py | 17 ----------------- apps/core/serializers/article.py | 8 ++++---- apps/core/views/viewsets/article.py | 25 ++++++++++++------------- tests/test_articles.py | 19 ------------------- 4 files changed, 16 insertions(+), 53 deletions(-) diff --git a/apps/core/permissions/article.py b/apps/core/permissions/article.py index 6873dfa0..50797eed 100644 --- a/apps/core/permissions/article.py +++ b/apps/core/permissions/article.py @@ -7,28 +7,11 @@ class ArticlePermission(permissions.IsAuthenticated): def has_object_permission(self, request, view, obj): if request.method not in permissions.SAFE_METHODS: return request.user.is_staff or request.user == obj.created_by - return super().has_object_permission(request, view, obj) -# TODO: Rename to ArticleGroupPermission -class ArticleKAISTPermission(permissions.BasePermission): - message = 'KAIST 구성원만 읽을 수 있는 게시물입니다.' - - def has_object_permission(self, request, view, obj: Article): - print('I am in retrieve in has_object_permission') - return obj.parent_board.group_has_read_access(request.user.profile.group) - - class ArticleReadPermission(permissions.BasePermission): message = '해당 게시물에 대한 읽기 권한이 없습니다.' def has_object_permission(self, request, view, obj: Article): return obj.parent_board.group_has_read_access(request.user.profile.group) - - -class ArticleWritePermission(permissions.BasePermission): - message = '해당 게시물에 대한 쓰기 권한이 없습니다.' - - def has_object_permission(self, request, view, obj: Article): - return obj.parent_board.group_has_write_access(request.user.profile.group) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index c15d4876..748dd304 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -2,7 +2,7 @@ from enum import Enum from django.utils.translation import gettext -from rest_framework import serializers +from rest_framework import serializers, exceptions from django.utils import timezone from apps.core.documents import ArticleDocument @@ -427,9 +427,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_read_perm = board.group_has_read_access(self.context['request'].user.profile.group) - if not user_has_read_perm: - raise serializers.ValidationError(gettext('Read permission denied')) + user_has_write_permission = board.group_has_write_access(self.context['request'].user.profile.group) + if not user_has_write_permission: + raise exceptions.PermissionDenied() return board diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index c07397f0..ec4fa060 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -21,7 +21,10 @@ Scrap, CommunicationArticle, ) from apps.core.filters.article import ArticleFilter -from apps.core.permissions.article import ArticlePermission, ArticleKAISTPermission +from apps.core.permissions.article import ( + ArticlePermission, + ArticleReadPermission +) from apps.core.serializers.article import ( ArticleSerializer, ArticleListActionSerializer, @@ -50,20 +53,24 @@ class ArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): } permission_classes = ( ArticlePermission, - ArticleKAISTPermission + ArticleReadPermission, ) action_permission_classes = { + 'create': ( + permissions.IsAuthenticated, + # Check WritePermission in ArticleCreateActionSerializer + ), 'vote_cancel': ( permissions.IsAuthenticated, - ArticleKAISTPermission + ArticleReadPermission, ), 'vote_positive': ( permissions.IsAuthenticated, - ArticleKAISTPermission + ArticleReadPermission, ), 'vote_negative': ( permissions.IsAuthenticated, - ArticleKAISTPermission + ArticleReadPermission, ), } @@ -133,14 +140,6 @@ def filter_queryset(self, queryset): ) return queryset - - def create(self, request, *args, **kwargs): - user_group = request.user.profile.group - board = Board.objects.get(pk=self.request.data['parent_board']) - if board.group_has_write_access(user_group): - return super().create(request, *args, **kwargs) - return response.Response({'message': gettext('Permission denied')}, - status=status.HTTP_403_FORBIDDEN) def perform_create(self, serializer): serializer.save( diff --git a/tests/test_articles.py b/tests/test_articles.py index e3ecbd93..ffcc950a 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -532,25 +532,6 @@ def test_self_vote(self): assert article.positive_vote_count == 0 assert article.negative_vote_count == 0 - @pytest.mark.usefixtures('set_kaist_articles') - def test_kaist_permission(self): - # 카이스트 구성원만 볼 수 있는 게시판에 대한 테스트 - def check_kaist_error(response): - assert response.status_code == status.HTTP_403_FORBIDDEN - assert 'KAIST' in response.data['detail'] # 에러 메세지 체크 - # 게시물 읽기 테스트 - check_kaist_error(self.http_request(self.non_kaist_user, 'get', f'articles/{self.kaist_article.id}')) - # 투표 테스트 - check_kaist_error( - self.http_request(self.non_kaist_user, 'post', f'articles/{self.kaist_article.id}/vote_positive') - ) - check_kaist_error( - self.http_request(self.non_kaist_user, 'post', f'articles/{self.kaist_article.id}/vote_negative') - ) - check_kaist_error( - self.http_request(self.non_kaist_user, 'post', f'articles/{self.kaist_article.id}/vote_cancel') - ) - @pytest.mark.usefixtures('set_readonly_board') def test_readonly_board(self): user_data = { From f69897c26f67fab9f78bf03bd34311fcb71e6679 Mon Sep 17 00:00:00 2001 From: Injoon Hwang <50509417+injoonH@users.noreply.github.com> Date: Thu, 2 Jun 2022 21:19:27 +0900 Subject: [PATCH 091/101] Fix typo 'test_ban_vote_cancellation_after_30' --- tests/test_articles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_articles.py b/tests/test_articles.py index ce9fafa9..b449de8f 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -575,7 +575,7 @@ def test_update_realname_article(self): assert result.get('name_type') == BoardNameType.REALNAME assert Article.objects.get(title=new_title).name_type == BoardNameType.REALNAME - def test_ban_vote_canclellation_after_30(self): + def test_ban_vote_cancellation_after_30(self): # SCHOOL_RESPONSE_VOTE_THRESHOLD is 3 in test users = [self.user, self.user2] for user in users: From a570c3eaf3b5eb33f0c1a8b46f72364a32350cf6 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Thu, 2 Jun 2022 21:31:06 +0900 Subject: [PATCH 092/101] Update permission checking for comment creation --- apps/core/views/viewsets/comment.py | 12 +++++++----- tests/test_comments.py | 2 +- tests/test_communication_article.py | 10 ++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index e6d15109..0e6232fb 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -57,13 +57,15 @@ class CommentViewSet(mixins.CreateModelMixin, } def create(self, request, *args, **kwargs): - if self.request.data['parent_article'] is None: - parent_article = Comment.objects.get(pk=self.request.data['parent_comment']).parent_article + if self.request.data.get('parent_article') is None: + comment_queryset = Comment.objects.all() + parent_comment = get_object_or_404(comment_queryset, pk=self.request.data['parent_comment']) + parent_article = parent_comment.parent_article else: - article_queryset = Article.objects.queryset_with_deleted + article_queryset = Article.objects.all() parent_article = get_object_or_404(article_queryset, pk=self.request.data['parent_article']) - if parent_article.deleted_at != timezone.datetime.min.replace(tzinfo=timezone.utc): - return response.Response(status=status.HTTP_410_GONE) + # TODO: Use CommentPermission for permission checking logic + # self.check_object_permissions(request, parent_article) user_group = request.user.profile.group if parent_article.parent_board.group_has_write_access(user_group): return super().create(request, *args, **kwargs) diff --git a/tests/test_comments.py b/tests/test_comments.py index 0c205551..f1f53e7e 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -396,7 +396,7 @@ def test_comment_on_deleted_article(self): 'parent_article': deleted_article.id, 'name_type': BoardNameType.REGULAR }) - assert res.status_code == status.HTTP_410_GONE + assert res.status_code == status.HTTP_404_NOT_FOUND def test_comment_on_report_hidden_article(self): report_hidden_article = Article.objects.create( diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 7e2d42f6..67907d68 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -54,7 +54,9 @@ def set_communication_board(request): ko_description='학교와의 게시판 (테스트)', en_description='With School (Test)', is_school_communication=True, - name_type=BoardNameType.REALNAME + name_type=BoardNameType.REALNAME, + read_access_mask=0b11011110, + write_access_mask=0b11011010 ) @@ -101,8 +103,8 @@ class TestCommunicationArticle(TestCase, RequestSetting): # ======================================================================= # def _get_communication_article_status(self, article): - res = self.http_request(self.user, 'get', f'articles/{article.id}').data - return res.get('communication_article_status') + res = self.http_request(self.user, 'get', f'articles/{article.id}') + return res.data.get('communication_article_status') def _create_dummy_users(self, num): dummy_users = [] @@ -152,7 +154,7 @@ def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_ } res = self.http_request(self.user, 'post', 'articles', article_data) - article = Article.objects.get(id=res.data.get('id')) + article = Article.objects.get(pk=res.data.get('id')) if status == SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD: pass From 2c15f28759431481f912549d9b05a14794128c33 Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Thu, 26 May 2022 21:35:02 +0900 Subject: [PATCH 093/101] Add ban cancellation vote over threshold in realname article --- apps/core/views/viewsets/article.py | 5 ++++ tests/test_articles.py | 41 +++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index ec4fa060..e1381c05 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -9,6 +9,7 @@ from ara import redis from ara.classes.viewset import ActionAPIViewSet +from ara.settings import SCHOOL_RESPONSE_VOTE_THRESHOLD from apps.core.models import ( Article, @@ -220,6 +221,10 @@ def retrieve(self, request, *args, **kwargs): def vote_cancel(self, request, *args, **kwargs): article = self.get_object() + if article.name_type == BoardNameType.REALNAME and article.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: + return response.Response({'message': gettext('Cannot cancel vote on more than 30 votes in realname article')}, + status=status.HTTP_405_METHOD_NOT_ALLOWED) + if article.is_hidden_by_reported(): return response.Response({'message': gettext('Cannot cancel vote on articles hidden by reports')}, status=status.HTTP_403_FORBIDDEN) diff --git a/tests/test_articles.py b/tests/test_articles.py index ffcc950a..fc7a0370 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -7,6 +7,7 @@ from apps.core.models import Article, Topic, Board, Block, Vote, Comment from apps.core.models.board import BoardNameType from apps.user.models import UserProfile +from ara.settings import SCHOOL_RESPONSE_VOTE_THRESHOLD from tests.conftest import RequestSetting, TestCase @@ -145,6 +146,25 @@ def set_articles(request): parent_board=request.cls.advertiser_accessible_board ) +@pytest.fixture(scope='class') +def set_realname_article(request): + """set_user_with_kaist_info,, set_realname_topic, set_realname_board 먼저 적용""" + request.cls.realname_article = Article.objects.create( + title='Realname Test Article', + content='Content of test realname article', + content_text='Content of test article in text', + name_type=BoardNameType.REALNAME, + is_content_sexual=False, + is_content_social=False, + hit_count=0, + positive_vote_count=0, + negative_vote_count=0, + created_by=request.cls.user_with_kaist_info, + parent_topic=request.cls.realname_topic, + parent_board=request.cls.realname_board, + commented_at=timezone.now() + ) + @pytest.fixture(scope='function') def set_kaist_articles(request): @@ -595,8 +615,8 @@ def test_deleting_with_comments(self): ).count() == 0 assert self.article.comment_count == 0 -@pytest.mark.usefixtures('set_user_client', 'set_user_with_kaist_info', 'set_user_without_kaist_info', - 'set_boards', 'set_topics', 'set_articles') +@pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', 'set_user_without_kaist_info', + 'set_boards', 'set_topics', 'set_articles', 'set_realname_article') class TestRealnameArticle(TestCase, RequestSetting): def test_get_realname_article(self): # kaist info가 있는 유저가 생성한 게시글 @@ -686,6 +706,23 @@ def test_update_realname_article(self): assert result.get('name_type') == BoardNameType.REALNAME assert Article.objects.get(title=new_title).name_type == BoardNameType.REALNAME + def test_ban_canclellation_vote_after_30(self): + # SCHOOL_RESPONSE_VOTE_THRESHOLD is 3 in test + users = [self.user, self.user2] + for user in users: + self.http_request(user, 'post', f'articles/{self.realname_article.id}/vote_positive') + + res1 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_positive') + article = Article.objects.get(id=self.realname_article.id) + assert res1.status_code == 200 + assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD + + res2 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_cancel') + article = Article.objects.get(id=self.realname_article.id) + assert res2.status_code == 405 + assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD + + @pytest.mark.usefixtures('set_user_client', 'set_user_client2', 'set_user_with_kaist_info', 'set_user_without_kaist_info', 'set_boards', 'set_topics', 'set_articles') class TestHiddenArticles(TestCase, RequestSetting): From d216ebdbf2b99488d32f43bb10d5ab5ce4a1bb5a Mon Sep 17 00:00:00 2001 From: Giyeong Kim Date: Fri, 27 May 2022 01:29:56 +0900 Subject: [PATCH 094/101] Fix typo, condition order in vote_cancel --- apps/core/views/viewsets/article.py | 8 ++++---- tests/test_articles.py | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index e1381c05..a86e4d83 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -221,14 +221,14 @@ def retrieve(self, request, *args, **kwargs): def vote_cancel(self, request, *args, **kwargs): article = self.get_object() - if article.name_type == BoardNameType.REALNAME and article.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: - return response.Response({'message': gettext('Cannot cancel vote on more than 30 votes in realname article')}, - status=status.HTTP_405_METHOD_NOT_ALLOWED) - if article.is_hidden_by_reported(): return response.Response({'message': gettext('Cannot cancel vote on articles hidden by reports')}, status=status.HTTP_403_FORBIDDEN) + if article.name_type == BoardNameType.REALNAME and article.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD: + return response.Response({'message': gettext('It is not available to cancel a vote for a real name article with more than 30 votes.')}, + status=status.HTTP_405_METHOD_NOT_ALLOWED) + if not Vote.objects.filter( voted_by=request.user, parent_article=article, diff --git a/tests/test_articles.py b/tests/test_articles.py index fc7a0370..47bbc58f 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -3,6 +3,7 @@ from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient +from rest_framework import status from apps.core.models import Article, Topic, Board, Block, Vote, Comment from apps.core.models.board import BoardNameType @@ -706,7 +707,7 @@ def test_update_realname_article(self): assert result.get('name_type') == BoardNameType.REALNAME assert Article.objects.get(title=new_title).name_type == BoardNameType.REALNAME - def test_ban_canclellation_vote_after_30(self): + def test_ban_vote_canclellation_after_30(self): # SCHOOL_RESPONSE_VOTE_THRESHOLD is 3 in test users = [self.user, self.user2] for user in users: @@ -714,12 +715,12 @@ def test_ban_canclellation_vote_after_30(self): res1 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_positive') article = Article.objects.get(id=self.realname_article.id) - assert res1.status_code == 200 + assert res1.status_code == status.HTTP_200_OK assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD res2 = self.http_request(self.user_without_kaist_info, 'post', f'articles/{self.realname_article.id}/vote_cancel') article = Article.objects.get(id=self.realname_article.id) - assert res2.status_code == 405 + assert res2.status_code == status.HTTP_405_METHOD_NOT_ALLOWED assert article.positive_vote_count == SCHOOL_RESPONSE_VOTE_THRESHOLD From 70e97e5e7ae6d126df141f02f5e4f031a370fe8a Mon Sep 17 00:00:00 2001 From: Injoon Hwang <50509417+injoonH@users.noreply.github.com> Date: Thu, 2 Jun 2022 21:19:27 +0900 Subject: [PATCH 095/101] Fix typo 'test_ban_vote_cancellation_after_30' --- tests/test_articles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_articles.py b/tests/test_articles.py index 47bbc58f..33329aa0 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -707,7 +707,7 @@ def test_update_realname_article(self): assert result.get('name_type') == BoardNameType.REALNAME assert Article.objects.get(title=new_title).name_type == BoardNameType.REALNAME - def test_ban_vote_canclellation_after_30(self): + def test_ban_vote_cancellation_after_30(self): # SCHOOL_RESPONSE_VOTE_THRESHOLD is 3 in test users = [self.user, self.user2] for user in users: From c71238c9559aea62d3115ea22e8728b941ab62e0 Mon Sep 17 00:00:00 2001 From: kjy2844 Date: Thu, 26 May 2022 23:17:20 +0900 Subject: [PATCH 096/101] Add is_admin, is_school_admin to UserProfile, days_left to Article --- apps/core/models/communication_article.py | 12 ++++++- apps/core/serializers/article.py | 20 +++++++++++ .../core/serializers/communication_article.py | 31 ----------------- apps/core/views/router.py | 6 ---- apps/core/views/viewsets/__init__.py | 1 - apps/core/views/viewsets/article.py | 3 ++ .../views/viewsets/communication_article.py | 29 ---------------- apps/user/models/user_profile.py | 8 +++++ apps/user/serializers/user_profile.py | 25 ++++++-------- tests/test_communication_article.py | 33 +++---------------- 10 files changed, 57 insertions(+), 111 deletions(-) delete mode 100644 apps/core/serializers/communication_article.py delete mode 100644 apps/core/views/viewsets/communication_article.py diff --git a/apps/core/models/communication_article.py b/apps/core/models/communication_article.py index f7b792ed..fa50c0ca 100644 --- a/apps/core/models/communication_article.py +++ b/apps/core/models/communication_article.py @@ -1,3 +1,6 @@ +import sys + +from cached_property import cached_property from django.db import models from django.utils import timezone from django.contrib import admin @@ -46,6 +49,13 @@ class Meta(MetaDataModel.Meta): def get_status_string(self) -> str: status_list = ['소통 중', '답변 대기 중', '답변 완료'] return status_list[self.school_response_status] - + + @cached_property + def days_left(self) -> int: + if self.response_deadline == timezone.datetime.min.replace(tzinfo=timezone.utc): + return sys.maxsize + else: + return (self.response_deadline.astimezone(timezone.localtime().tzinfo) - timezone.localtime()).days + def __str__(self): return self.article.title diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 748dd304..a5cad97d 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -1,3 +1,4 @@ +import sys import typing from enum import Enum @@ -336,6 +337,16 @@ def get_my_comment_profile(self, obj): read_only=True, ) + days_left = serializers.SerializerMethodField( + read_only=True, + ) + + @staticmethod + def get_days_left(obj): + if hasattr(obj, 'communication_article'): + return obj.communication_article.days_left + return None + @staticmethod def get_communication_article_status(obj): if hasattr(obj, 'communication_article'): @@ -375,6 +386,9 @@ class ArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSeriali read_only=True, ) + days_left = serializers.SerializerMethodField( + read_only=True, + ) def get_attachment_type(self, obj) -> str: if not self.visible_verdict(obj): return ArticleAttachmentType.NONE.value @@ -400,6 +414,12 @@ def get_communication_article_status(obj): return obj.communication_article.school_response_status return None + @staticmethod + def get_days_left(obj): + if hasattr(obj, 'communication_article'): + return obj.communication_article.days_left + return None + class BestArticleListActionSerializer(HiddenSerializerFieldMixin, BaseArticleSerializer): title = serializers.SerializerMethodField( diff --git a/apps/core/serializers/communication_article.py b/apps/core/serializers/communication_article.py deleted file mode 100644 index 0e5885ba..00000000 --- a/apps/core/serializers/communication_article.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys - -from django.utils import timezone -from rest_framework import serializers - -from ara.classes.serializers import MetaDataModelSerializer -from apps.core.models.communication_article import CommunicationArticle - - -class BaseCommunicationArticleSerializer(MetaDataModelSerializer): - class Meta: - model = CommunicationArticle - fields = '__all__' - read_only_fields = ( - 'article', - 'response_deadline', - 'school_response_status', - ) - - -class CommunicationArticleSerializer(BaseCommunicationArticleSerializer): - days_left = serializers.SerializerMethodField( - read_only=True, - ) - - @staticmethod - def get_days_left(obj) -> int: - if obj.response_deadline == timezone.datetime.min.replace(tzinfo=timezone.utc): - return sys.maxsize - else: - return (obj.response_deadline.astimezone(timezone.localtime().tzinfo) - timezone.localtime()).days \ No newline at end of file diff --git a/apps/core/views/router.py b/apps/core/views/router.py index 13f27c65..1ec37c6e 100644 --- a/apps/core/views/router.py +++ b/apps/core/views/router.py @@ -63,9 +63,3 @@ prefix=r'best_searches', viewset=viewsets.BestSearchViewSet, ) - -# CommunicationArticleViewSet -router.register( - prefix=r'communication_articles', - viewset=viewsets.CommunicationArticleViewSet, -) diff --git a/apps/core/views/viewsets/__init__.py b/apps/core/views/viewsets/__init__.py index a280de75..89e9160d 100644 --- a/apps/core/views/viewsets/__init__.py +++ b/apps/core/views/viewsets/__init__.py @@ -8,4 +8,3 @@ from .scrap import * from .faq import * from .best_search import * -from .communication_article import * diff --git a/apps/core/views/viewsets/article.py b/apps/core/views/viewsets/article.py index a86e4d83..01070933 100644 --- a/apps/core/views/viewsets/article.py +++ b/apps/core/views/viewsets/article.py @@ -215,6 +215,9 @@ def retrieve(self, request, *args, **kwargs): pipe.execute(raise_on_error=True) serialized = ArticleSerializer(article, context={'request': request, 'override_hidden': override_hidden}) + + if not request.user.profile.is_school_admin: + serialized.days_left = None return Response(serialized.data) @decorators.action(detail=True, methods=['post']) diff --git a/apps/core/views/viewsets/communication_article.py b/apps/core/views/viewsets/communication_article.py deleted file mode 100644 index 0ffdcfee..00000000 --- a/apps/core/views/viewsets/communication_article.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.utils import timezone -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import status, filters, viewsets, response, permissions - -from apps.core.permissions.communication_article import CommunicationArticleAdminPermission -from ara.classes.viewset import ActionAPIViewSet - -from apps.core.models.communication_article import CommunicationArticle -from apps.core.serializers.communication_article import CommunicationArticleSerializer - - -class CommunicationArticleViewSet(viewsets.ModelViewSet, ActionAPIViewSet): - queryset = CommunicationArticle.objects.all() - serializer_class = CommunicationArticleSerializer - action_serializer_class = { - 'list': CommunicationArticleSerializer, - } - permission_classes = ( - permissions.IsAuthenticated, - CommunicationArticleAdminPermission, - ) - filter_backends = [filters.OrderingFilter, DjangoFilterBackend] - - # usage: /api/communication_articles/?ordering=created_at - ordering_fields = ['article__positive_vote_count'] - ordering = ['-article__positive_vote_count'] # default: 추천수 내림차순 - - # usage: /api/communication_articles/?school_response_status=1 - filterset_fields = ['school_response_status'] diff --git a/apps/user/models/user_profile.py b/apps/user/models/user_profile.py index bc4f8f5b..b4424919 100644 --- a/apps/user/models/user_profile.py +++ b/apps/user/models/user_profile.py @@ -136,3 +136,11 @@ def realname(self) -> str: user_realname = json.loads(sso_info["kaist_info"])["ku_kname"] if sso_info["kaist_info"] else sso_info["last_name"] + sso_info["first_name"] return user_realname + + @cached_property + def is_official(self) -> bool: + return self.group in UserProfile.OFFICIAL_GROUPS + + @cached_property + def is_school_admin(self) -> bool: + return self.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN diff --git a/apps/user/serializers/user_profile.py b/apps/user/serializers/user_profile.py index 6b71df03..eb51dae3 100644 --- a/apps/user/serializers/user_profile.py +++ b/apps/user/serializers/user_profile.py @@ -10,6 +10,8 @@ class BaseUserProfileSerializer(MetaDataModelSerializer): email = serializers.SerializerMethodField() + is_school_admin = serializers.SerializerMethodField() + is_official = serializers.SerializerMethodField() class Meta: model = UserProfile @@ -21,6 +23,14 @@ def get_email(obj) -> typing.Optional[str]: return None return obj.email + @staticmethod + def get_is_school_admin(obj) -> bool: + return obj.is_school_admin + + @staticmethod + def get_is_official(obj) -> bool: + return obj.is_official + class UserProfileSerializer(BaseUserProfileSerializer): extra_preferences = serializers.JSONField( @@ -65,21 +75,6 @@ class Meta(BaseUserProfileSerializer.Meta): 'is_school_admin', ) - is_official = serializers.SerializerMethodField( - read_only=True, - ) - is_school_admin = serializers.SerializerMethodField( - read_only=True, - ) - - @staticmethod - def get_is_official(obj) -> bool: - return obj.group in UserProfile.OFFICIAL_GROUPS - - @staticmethod - def get_is_school_admin(obj) -> bool: - return obj.group == UserProfile.UserGroup.COMMUNICATION_BOARD_ADMIN - class MyPageUserProfileSerializer(BaseUserProfileSerializer): num_articles = serializers.SerializerMethodField() diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index 67907d68..ffe5d743 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -8,7 +8,6 @@ from django.utils import timezone from django.contrib.auth.models import User -from rest_framework import status from rest_framework.test import APIClient from rest_framework.status import HTTP_200_OK @@ -17,7 +16,6 @@ from apps.core.models import Article, Board from apps.core.models.board import BoardNameType from apps.core.models.communication_article import CommunicationArticle, SchoolResponseStatus -from apps.core.serializers.communication_article import CommunicationArticleSerializer from apps.user.models import UserProfile from ara.settings import ANSWER_PERIOD @@ -266,11 +264,11 @@ def test_days_left(self): article = self._create_article_with_status(status) # days_left가 설정되기 전 - res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + res = self.http_request(self.school_admin, 'get', f'articles/{article.id}') assert res.data.get('days_left') == sys.maxsize - + # self._add_positive_votes(article, SCHOOL_RESPONSE_VOTE_THRESHOLD) - res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + res = self.http_request(self.school_admin, 'get', f'articles/{article.id}') assert res.data.get('days_left') == ANSWER_PERIOD def test_days_left_in_local_timezone(self): @@ -282,11 +280,11 @@ def test_days_left_in_local_timezone(self): # localtime 기준으로 오늘에서 내일로 넘어갈 때 D-day 변경되는지 확인 with patch.object(timezone, 'localtime', return_value=today): - res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + res = self.http_request(self.school_admin, 'get', f'articles/{article.id}') assert res.data.get('days_left') == ANSWER_PERIOD with patch.object(timezone, 'localtime', return_value=tomorrow): - res = self.http_request(self.school_admin, 'get', f'communication_articles/{article.id}') + res = self.http_request(self.school_admin, 'get', f'articles/{article.id}') assert res.data.get('days_left') == ANSWER_PERIOD - 1 @@ -388,24 +386,3 @@ def test_anonymous_comment(self): } res = self.http_request(self.user, 'post', 'comments', comment_data).data assert res.get('name_type') == BoardNameType.REALNAME - - # ======================================================================= # - # Permission # - # ======================================================================= # - def test_admin_can_view_admin_api_list(self): - res = self.http_request(self.school_admin, 'get', 'communication_articles') - assert res.status_code == 200 - assert res.data.get('num_items') == CommunicationArticle.objects.all().count() - - def test_admin_can_view_admin_api_get(self): - res = self.http_request(self.school_admin, 'get', f'communication_articles/{self.article.id}') - assert res.status_code == 200 - assert res.data.get('article') == self.communication_article.article.id - - def test_user_cannot_view_admin_api_list(self): - res = self.http_request(self.user, 'get', 'communication_articles') - assert res.status_code == status.HTTP_403_FORBIDDEN - - def test_user_cannot_view_admin_api_get(self): - res = self.http_request(self.user, 'get', f'communication_articles/{self.article.id}') - assert res.status_code == status.HTTP_403_FORBIDDEN From 708de3c328841bc1bb94ab0b78fc845204007eeb Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Thu, 2 Jun 2022 22:57:54 +0900 Subject: [PATCH 097/101] Add is_official and is_school_admin to realname user_profile serializer --- apps/core/models/comment.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index 2d4bd71f..53022b7f 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -184,7 +184,7 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'profile': { 'picture': default_storage.url(user_profile_picture), 'nickname': user_name, - 'user': user_hash + 'user': user_hash, } } @@ -200,7 +200,10 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'profile': { 'picture': default_storage.url(user_profile_picture), 'nickname': user_realname, - 'user': user_realname + 'user': user_realname, + 'is_official': self.created_by.profile.is_official, + 'is_school_admin': self.created_by.profile.is_school_admin, + }, } From a18ed7c5440a7b9dd8aa37eb3b878d249cde7daf Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Thu, 2 Jun 2022 23:22:08 +0900 Subject: [PATCH 098/101] Remove is_school_admin from user_profile serializer --- apps/core/models/comment.py | 7 ++++--- apps/user/serializers/user_profile.py | 6 ------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/core/models/comment.py b/apps/core/models/comment.py index 53022b7f..c7908e7d 100644 --- a/apps/core/models/comment.py +++ b/apps/core/models/comment.py @@ -185,6 +185,8 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'picture': default_storage.url(user_profile_picture), 'nickname': user_name, 'user': user_hash, + 'is_official': None, + 'is_school_admin': None, } } @@ -201,9 +203,8 @@ def postprocessed_created_by(self) -> Union[settings.AUTH_USER_MODEL, Dict]: 'picture': default_storage.url(user_profile_picture), 'nickname': user_realname, 'user': user_realname, - 'is_official': self.created_by.profile.is_official, - 'is_school_admin': self.created_by.profile.is_school_admin, - + 'is_official': None, + 'is_school_admin': self.created_by.profile.is_school_admin if parent_article.parent_board.is_school_communication else None }, } diff --git a/apps/user/serializers/user_profile.py b/apps/user/serializers/user_profile.py index eb51dae3..e28e8705 100644 --- a/apps/user/serializers/user_profile.py +++ b/apps/user/serializers/user_profile.py @@ -10,7 +10,6 @@ class BaseUserProfileSerializer(MetaDataModelSerializer): email = serializers.SerializerMethodField() - is_school_admin = serializers.SerializerMethodField() is_official = serializers.SerializerMethodField() class Meta: @@ -23,10 +22,6 @@ def get_email(obj) -> typing.Optional[str]: return None return obj.email - @staticmethod - def get_is_school_admin(obj) -> bool: - return obj.is_school_admin - @staticmethod def get_is_official(obj) -> bool: return obj.is_official @@ -72,7 +67,6 @@ class Meta(BaseUserProfileSerializer.Meta): 'nickname', 'user', 'is_official', - 'is_school_admin', ) From 356ac11f8ccff445f5c5285cb067f01ea787c1e2 Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Thu, 2 Jun 2022 23:31:07 +0900 Subject: [PATCH 099/101] Remove unused imports in article serializer --- apps/core/serializers/article.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index a5cad97d..4a7ff1bf 100644 --- a/apps/core/serializers/article.py +++ b/apps/core/serializers/article.py @@ -1,10 +1,8 @@ -import sys import typing from enum import Enum from django.utils.translation import gettext from rest_framework import serializers, exceptions -from django.utils import timezone from apps.core.documents import ArticleDocument from apps.core.models import Article, Board, Block, Scrap, ArticleHiddenReason, Comment From ec1046bbd14c102b01a216039e009c66ebbb5fba Mon Sep 17 00:00:00 2001 From: Jeesoo Yoon Date: Fri, 3 Jun 2022 16:09:55 +0900 Subject: [PATCH 100/101] Fix CommentCreateSerializer to use postprocess_created_by --- apps/core/serializers/comment.py | 3 +-- tests/test_communication_article.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/core/serializers/comment.py b/apps/core/serializers/comment.py index a3b3268b..c792b90d 100644 --- a/apps/core/serializers/comment.py +++ b/apps/core/serializers/comment.py @@ -94,8 +94,7 @@ class Meta(BaseCommentSerializer.Meta): 'created_by', ) - from apps.user.serializers.user import PublicUserSerializer - created_by = PublicUserSerializer( + created_by = serializers.SerializerMethodField( read_only=True, ) diff --git a/tests/test_communication_article.py b/tests/test_communication_article.py index ffe5d743..937018af 100644 --- a/tests/test_communication_article.py +++ b/tests/test_communication_article.py @@ -137,7 +137,8 @@ def _add_admin_comment(self, article): 'created_by': self.school_admin.id, 'parent_article': article.id } - self.http_request(self.school_admin, 'post', 'comments', comment_data) + res = self.http_request(self.school_admin, 'post', 'comments', comment_data) + assert res.data.get('created_by').get('profile').get('is_school_admin') # status를 가지는 communication_article 반환 def _create_article_with_status(self, status=SchoolResponseStatus.BEFORE_UPVOTE_THRESHOLD): From ac21fbe79f93e8c3203b1408171e03f1a6bb7b72 Mon Sep 17 00:00:00 2001 From: Injoon Hwang Date: Wed, 10 Aug 2022 03:08:24 +0900 Subject: [PATCH 101/101] Add comment_access_mask --- .../0043_board_comment_access_mask.py | 18 ++++++++++ apps/core/models/article.py | 5 +-- apps/core/models/board.py | 33 ++++++++++++++++--- apps/core/permissions/article.py | 5 ++- apps/core/serializers/article.py | 6 ++-- apps/core/serializers/board.py | 10 ++++-- apps/core/views/viewsets/comment.py | 6 ++-- tests/test_articles.py | 10 ++++-- 8 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 apps/core/migrations/0043_board_comment_access_mask.py diff --git a/apps/core/migrations/0043_board_comment_access_mask.py b/apps/core/migrations/0043_board_comment_access_mask.py new file mode 100644 index 00000000..3e735c2c --- /dev/null +++ b/apps/core/migrations/0043_board_comment_access_mask.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.9 on 2022-08-09 17:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0042_add_read_write_access_masks'), + ] + + operations = [ + migrations.AddField( + model_name='board', + name='comment_access_mask', + field=models.SmallIntegerField(default=254, verbose_name='댓글 권한'), + ), + ] diff --git a/apps/core/models/article.py b/apps/core/models/article.py index 9502d566..76939f19 100644 --- a/apps/core/models/article.py +++ b/apps/core/models/article.py @@ -21,7 +21,7 @@ from .report import Report from .comment import Comment from .communication_article import SchoolResponseStatus -from .board import BoardNameType +from .board import BoardNameType, BoardAccessPermissionType class ArticleHiddenReason(str, Enum): @@ -262,7 +262,8 @@ def hidden_reasons(self, user: settings.AUTH_USER_MODEL) -> typing.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_read_access(user.profile.group): + if not self.parent_board.group_has_access_permission( + BoardAccessPermissionType.READ, user.profile.group): reasons.append(ArticleHiddenReason.ACCESS_DENIED_CONTENT) return reasons diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 944a36c3..a589ad93 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -11,6 +11,12 @@ class BoardNameType(IntEnum): REALNAME = 2 +class BoardAccessPermissionType(IntEnum): + READ = 0 + WRITE = 1 + COMMENT = 2 + + class Board(MetaDataModel): class Meta(MetaDataModel.Meta): verbose_name = '게시판' @@ -55,6 +61,12 @@ class Meta(MetaDataModel.Meta): null=False, verbose_name='쓰기 권한' ) + comment_access_mask = models.SmallIntegerField( + # UNAUTHORIZED 제외 모든 사용자 댓글 권한 부여 + default=0b11111110, + null=False, + verbose_name='댓글 권한' + ) is_readonly = models.BooleanField( verbose_name='읽기 전용 게시판', @@ -116,9 +128,20 @@ class Meta(MetaDataModel.Meta): def __str__(self) -> str: return self.ko_name - - def group_has_read_access(self, group: int) -> bool: - return (self.read_access_mask & (1 << group)) > 0 - def group_has_write_access(self, group: int) -> bool: - return (self.write_access_mask & (1 << group)) > 0 + 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 + + return (mask & (1 << group)) > 0 diff --git a/apps/core/permissions/article.py b/apps/core/permissions/article.py index 50797eed..60e9f56a 100644 --- a/apps/core/permissions/article.py +++ b/apps/core/permissions/article.py @@ -1,6 +1,7 @@ from rest_framework import permissions from apps.core.models import Article +from apps.core.models.board import BoardAccessPermissionType class ArticlePermission(permissions.IsAuthenticated): @@ -14,4 +15,6 @@ class ArticleReadPermission(permissions.BasePermission): message = '해당 게시물에 대한 읽기 권한이 없습니다.' def has_object_permission(self, request, view, obj: Article): - return obj.parent_board.group_has_read_access(request.user.profile.group) + return obj.parent_board.group_has_access_permission( + BoardAccessPermissionType.READ, + request.user.profile.group) diff --git a/apps/core/serializers/article.py b/apps/core/serializers/article.py index 4a7ff1bf..918abb31 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, Board, Block, Scrap, ArticleHiddenReason, Comment -from apps.core.models.board import BoardNameType +from apps.core.models.board import BoardNameType, BoardAccessPermissionType from apps.core.serializers.board import BoardSerializer from apps.core.serializers.mixins.hidden import HiddenSerializerMixin, HiddenSerializerFieldMixin from apps.core.serializers.topic import TopicSerializer @@ -445,7 +445,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_write_access(self.context['request'].user.profile.group) + user_has_write_permission = board.group_has_access_permission( + BoardAccessPermissionType.WRITE, + self.context['request'].user.profile.group) 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 3ec11edc..f4d3da19 100644 --- a/apps/core/serializers/board.py +++ b/apps/core/serializers/board.py @@ -1,7 +1,7 @@ from rest_framework import serializers from ara.classes.serializers import MetaDataModelSerializer -from apps.core.models import Board +from apps.core.models.board import Board, BoardAccessPermissionType class BaseBoardSerializer(MetaDataModelSerializer): @@ -26,8 +26,12 @@ class BoardDetailActionSerializer(BaseBoardSerializer): def get_user_readable(self, obj): user = self.context['request'].user - return obj.group_has_read_access(user.profile.group) + return obj.group_has_access_permission( + BoardAccessPermissionType.READ, + user.profile.group) def get_user_writable(self, obj): user = self.context['request'].user - return obj.group_has_write_access(user.profile.group) + return obj.group_has_access_permission( + BoardAccessPermissionType.WRITE, + user.profile.group) diff --git a/apps/core/views/viewsets/comment.py b/apps/core/views/viewsets/comment.py index 0e6232fb..cd33d868 100644 --- a/apps/core/views/viewsets/comment.py +++ b/apps/core/views/viewsets/comment.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext from rest_framework import mixins, status, response, decorators, serializers, permissions -from apps.core.models.board import BoardNameType +from apps.core.models.board import BoardAccessPermissionType from ara.classes.viewset import ActionAPIViewSet @@ -66,8 +66,10 @@ def create(self, request, *args, **kwargs): parent_article = get_object_or_404(article_queryset, pk=self.request.data['parent_article']) # TODO: Use CommentPermission for permission checking logic # self.check_object_permissions(request, parent_article) + + # Check permission user_group = request.user.profile.group - if parent_article.parent_board.group_has_write_access(user_group): + if parent_article.parent_board.group_has_access_permission(BoardAccessPermissionType.COMMENT, user_group): return super().create(request, *args, **kwargs) return response.Response({'message': gettext('Permission denied')}, status=status.HTTP_403_FORBIDDEN) diff --git a/tests/test_articles.py b/tests/test_articles.py index 33329aa0..9e352970 100644 --- a/tests/test_articles.py +++ b/tests/test_articles.py @@ -6,7 +6,7 @@ from rest_framework import status from apps.core.models import Article, Topic, Board, Block, Vote, Comment -from apps.core.models.board import BoardNameType +from apps.core.models.board import BoardNameType, BoardAccessPermissionType from apps.user.models import UserProfile from ara.settings import SCHOOL_RESPONSE_VOTE_THRESHOLD from tests.conftest import RequestSetting, TestCase @@ -366,7 +366,9 @@ 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_read_access(user.profile.group): + if article.parent_board.group_has_access_permission( + BoardAccessPermissionType.READ, + user.profile.group): assert res.status_code == status.HTTP_200_OK assert res.data['id'] == article.id else: @@ -392,7 +394,9 @@ def test_check_write_permission_when_create(self): 'parent_board': board.id }) - if board.group_has_write_access(user.profile.group): + if board.group_has_access_permission( + BoardAccessPermissionType.WRITE, + user.profile.group): assert res.status_code == status.HTTP_201_CREATED else: assert res.status_code == status.HTTP_403_FORBIDDEN