diff --git a/README.md b/README.md index 14f2c22f..0a0c6731 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,14 @@ ### Create & Activate Virtual Environment ```bash -python3 -m venv env # should be python 3.8 -source env/bin/activate +python3 -m pip install pipenv # It must be Python 3.11 +pipenv shell ``` ### Install Requirements ```bash -pip install poetry -poetry install # in production - $ poetry install --no-dev +pipenv install --dev # `--dev` flag for development pre-commit install ``` @@ -175,22 +174,21 @@ For managing docker images, we are using AWS ECR, `newara` repository. ### Interpreter -- `Python 3.7` -- `poetry` is used as package manager +- `Python 3.11` +- `Pipenv` is used as package manager - When adding libraries to the virtual environment, you should not use `pip`. - Rather, use `poetry add` command. Refer to [this link](https://python-poetry.org/docs/cli/) - for poetry commands. + Rather, use `pipenv` command. Refer to [this link](https://pipenv.pypa.io/en/latest/commands/) + for pipenv commands. ### Framework -- `Django 3.2` +- `Django 4.2` - `djangorestframework 3.14` ### Database -- MySQL (default) -- `mysqlclient 2.1` -- `django-mysql 3.12` +- MySQL 8.0 +- `mysqlclient 2.2` Works with MySQL for Linux & macOS, not tested in Windows. Timezone is automatically adjusted. It is strongly recommended to set default charset of @@ -199,7 +197,7 @@ database or MySQL server to `utf8mb4`. ### Storage - AWS S3 -- `django-s3-storage 0.13` +- `django-s3-storage 0.14` Two buckets are used - one for storing static files, one for media files that users upload. Go to django-s3-storage documentation for required permissions. @@ -211,7 +209,7 @@ users upload. Go to django-s3-storage documentation for required permissions. ### API Documentation -- `drf-yasg 1.21` +- `drf-spectacular 0.26` --- diff --git a/apps/core/admin.py b/apps/core/admin.py index 47780bdd..d6f4415e 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -10,6 +10,7 @@ BestComment, BestSearch, Board, + BoardGroup, Comment, CommentDeleteLog, CommunicationArticle, @@ -17,7 +18,6 @@ Topic, ) from ara.classes.admin import MetaDataModelAdmin -from ara.settings import MIN_TIME class HiddenContentListFilter(admin.SimpleListFilter): @@ -42,6 +42,7 @@ class BoardAdmin(MetaDataModelAdmin): list_display = ( "ko_name", "en_name", + "group", "is_readonly", "is_hidden", ) @@ -51,6 +52,15 @@ class BoardAdmin(MetaDataModelAdmin): ) +@admin.register(BoardGroup) +class BoardGroupAdmin(admin.ModelAdmin): + list_display = ( + "slug", + "ko_name", + "en_name", + ) + + @admin.register(Topic) class TopicAdmin(MetaDataModelAdmin): list_display = ( diff --git a/apps/core/migrations/0053_remove_board_banner_url_and_more.py b/apps/core/migrations/0053_remove_board_banner_url_and_more.py new file mode 100644 index 00000000..4deb1456 --- /dev/null +++ b/apps/core/migrations/0053_remove_board_banner_url_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.3 on 2023-07-21 12:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0052_alter_board_slug"), + ] + + operations = [ + migrations.RemoveField( + model_name="board", + name="banner_url", + ), + migrations.AlterField( + model_name="board", + name="en_banner_description", + field=models.TextField( + blank=True, default="", verbose_name="게시판 배너에 삽입되는 영문 소개" + ), + ), + migrations.AlterField( + model_name="board", + name="ko_banner_description", + field=models.TextField( + blank=True, default="", verbose_name="게시판 배너에 삽입되는 국문 소개" + ), + ), + ] diff --git a/apps/core/migrations/0054_boardgroup.py b/apps/core/migrations/0054_boardgroup.py new file mode 100644 index 00000000..c656e44b --- /dev/null +++ b/apps/core/migrations/0054_boardgroup.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.3 on 2023-07-21 12:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0053_remove_board_banner_url_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="BoardGroup", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "ko_name", + models.CharField(max_length=64, verbose_name="게시판 그룹 국문 이름"), + ), + ( + "en_name", + models.CharField(max_length=64, verbose_name="게시판 그룹 영문 이름"), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="슬러그"), + ), + ], + options={ + "verbose_name": "게시판 그룹", + "verbose_name_plural": "게시판 그룹 목록", + }, + ), + ] diff --git a/apps/core/migrations/0055_remove_board_group_id_board_group.py b/apps/core/migrations/0055_remove_board_group_id_board_group.py new file mode 100644 index 00000000..dbb1a20a --- /dev/null +++ b/apps/core/migrations/0055_remove_board_group_id_board_group.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.3 on 2023-07-22 14:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0054_boardgroup"), + ] + + operations = [ + migrations.RemoveField( + model_name="board", + name="group_id", + ), + migrations.AddField( + model_name="board", + name="group", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="boards", + to="core.boardgroup", + verbose_name="게시판 그룹", + ), + ), + ] diff --git a/apps/core/models/__init__.py b/apps/core/models/__init__.py index 5d99a686..52eaaa10 100644 --- a/apps/core/models/__init__.py +++ b/apps/core/models/__init__.py @@ -6,6 +6,7 @@ from .best_search import * from .block import * from .board import * +from .board_group import * from .comment import * from .comment_log import * from .communication_article import * diff --git a/apps/core/models/board.py b/apps/core/models/board.py index 6bbeec4b..1fdb4716 100644 --- a/apps/core/models/board.py +++ b/apps/core/models/board.py @@ -4,6 +4,7 @@ from django_extensions.db.fields import AutoSlugField from ara.db.models import MetaDataModel +from .board_group import BoardGroup class NameType(IntFlag): @@ -73,9 +74,13 @@ class Board(MetaDataModel): default=False, help_text="학교 소통 게시판 글임을 표시", ) - group_id = models.IntegerField( - verbose_name="그룹 ID", - default=1, + group: BoardGroup = models.ForeignKey( + to="core.BoardGroup", + on_delete=models.SET_NULL, + related_name="boards", + verbose_name="게시판 그룹", + null=True, + default=None, ) banner_image = models.ImageField( verbose_name="게시판 배너 이미지", @@ -85,20 +90,12 @@ class Board(MetaDataModel): ko_banner_description = models.TextField( verbose_name="게시판 배너에 삽입되는 국문 소개", blank=True, - null=True, - default=None, + default="", ) en_banner_description = models.TextField( verbose_name="게시판 배너에 삽입되는 영문 소개", blank=True, - null=True, - default=None, - ) - banner_url = models.TextField( - verbose_name="게시판 배너를 클릭 시에 이동하는 링크", - blank=True, - null=True, - default=None, + default="", ) top_threshold = models.SmallIntegerField( verbose_name="인기글 달성 기준 좋아요 개수", diff --git a/apps/core/models/board_group.py b/apps/core/models/board_group.py new file mode 100644 index 00000000..3f78fc04 --- /dev/null +++ b/apps/core/models/board_group.py @@ -0,0 +1,24 @@ +from django.db import models + + +class BoardGroup(models.Model): + ko_name = models.CharField( + verbose_name="게시판 그룹 국문 이름", + max_length=64, + ) + en_name = models.CharField( + verbose_name="게시판 그룹 영문 이름", + max_length=64, + ) + slug = models.SlugField( + verbose_name="슬러그", + max_length=32, + unique=True, + ) + + class Meta: + verbose_name = "게시판 그룹" + verbose_name_plural = "게시판 그룹 목록" + + def __str__(self) -> str: + return self.ko_name diff --git a/apps/core/serializers/board.py b/apps/core/serializers/board.py index ba186a2a..7415e5fd 100644 --- a/apps/core/serializers/board.py +++ b/apps/core/serializers/board.py @@ -14,12 +14,19 @@ class Meta: "en_name", "is_readonly", "name_type", - "group_id", + "group", "banner_image", "ko_banner_description", "en_banner_description", "top_threshold", ] + depth = 1 + + +class SimpleBoardSerializer(serializers.ModelSerializer): + class Meta: + model = Board + fields = ["id", "slug", "ko_name", "en_name"] class BoardSerializer(BaseBoardSerializer): diff --git a/apps/core/serializers/board_group.py b/apps/core/serializers/board_group.py new file mode 100644 index 00000000..0634b93b --- /dev/null +++ b/apps/core/serializers/board_group.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from apps.core.models.board_group import BoardGroup +from apps.core.serializers.board import SimpleBoardSerializer + + +class BoardGroupSerializer(serializers.ModelSerializer): + boards = SimpleBoardSerializer(many=True, read_only=True) + + class Meta: + model = BoardGroup + fields = ["id", "slug", "ko_name", "en_name", "boards"] + depth = 1 diff --git a/apps/core/views/router.py b/apps/core/views/router.py index bcd4c375..7d83076a 100644 --- a/apps/core/views/router.py +++ b/apps/core/views/router.py @@ -10,6 +10,11 @@ viewset=viewsets.BoardViewSet, ) +router.register( + prefix=r"board_groups", + viewset=viewsets.BoardGroupViewSet, +) + # ArticleViewSet router.register( prefix=r"articles", diff --git a/apps/core/views/viewsets/__init__.py b/apps/core/views/viewsets/__init__.py index 429cc1fe..0a079b8f 100644 --- a/apps/core/views/viewsets/__init__.py +++ b/apps/core/views/viewsets/__init__.py @@ -3,6 +3,7 @@ from .best_search import * from .block import * from .board import * +from .board_group import * from .comment import * from .faq import * from .notification import * diff --git a/apps/core/views/viewsets/board_group.py b/apps/core/views/viewsets/board_group.py new file mode 100644 index 00000000..31b31429 --- /dev/null +++ b/apps/core/views/viewsets/board_group.py @@ -0,0 +1,12 @@ +from rest_framework import permissions, viewsets + +from apps.core.models import BoardGroup +from apps.core.serializers.board_group import BoardGroupSerializer + + +class BoardGroupViewSet(viewsets.ReadOnlyModelViewSet): + queryset = BoardGroup.objects.all() + serializer_class = BoardGroupSerializer + permission_classes = [permissions.IsAuthenticated] + lookup_field = "slug" + pagination_class = None