diff --git a/base/messages.py b/base/messages.py index 82d471a..a13c1d6 100644 --- a/base/messages.py +++ b/base/messages.py @@ -65,7 +65,12 @@ ERROR_CODE = { "2036": "Choose valid user", # log in multiple device msg "2037": "You are already log in another device", - "2038": "Choose valid action for task" + "2038": "Choose valid action for task", + "2039": "Add at least one article card or maximum 6", + "2040": "Add at least 5 article survey or maximum 10", + "2041": "Article with given id doesn't exist.", + "2042": "Article Card with given id doesn't exist.", + "2043": "Article Survey with given id doesn't exist." } """Success message code""" SUCCESS_CODE = { @@ -105,6 +110,11 @@ SUCCESS_CODE = { "3024": "Junior request is rejected successfully", "3025": "Task is approved successfully", "3026": "Task is rejected successfully", + "3027": "Article has been created successfully.", + "3028": "Article has been updated successfully.", + "3029": "Article has been deleted successfully.", + "3030": "Article Card has been removed successfully.", + "3031": "Article Survey has been removed successfully.", } """status code error""" STATUS_CODE_ERROR = { diff --git a/web_admin/__init__.py b/web_admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web_admin/admin.py b/web_admin/admin.py new file mode 100644 index 0000000..b1df483 --- /dev/null +++ b/web_admin/admin.py @@ -0,0 +1,32 @@ +""" +web_admin admin file +""" +# django imports +from django.contrib import admin + +# local imports +from web_admin.models import Article, ArticleCard, ArticleSurvey, SurveyOption + + +@admin.register(Article) +class ArticleAdmin(admin.ModelAdmin): + """Article Admin""" + list_display = ['id', 'title', 'description', 'is_published', 'is_deleted'] + + +@admin.register(ArticleCard) +class ArticleCardAdmin(admin.ModelAdmin): + """Article Card Admin""" + list_display = ['id', 'article', 'title', 'description', 'image'] + + +@admin.register(ArticleSurvey) +class ArticleSurveyAdmin(admin.ModelAdmin): + """Article Survey Admin""" + list_display = ['id', 'article', 'question', 'points'] + + +@admin.register(SurveyOption) +class SurveyOptionAdmin(admin.ModelAdmin): + """Survey Option Admin""" + list_display = ['id', 'survey', 'option', 'is_answer'] diff --git a/web_admin/apps.py b/web_admin/apps.py new file mode 100644 index 0000000..2842e02 --- /dev/null +++ b/web_admin/apps.py @@ -0,0 +1,13 @@ +""" +web_admin app file +""" +# django imports +from django.apps import AppConfig + + +class WebAdminConfig(AppConfig): + """ + web admin app config + """ + default_auto_field = 'django.db.models.BigAutoField' + name = 'web_admin' diff --git a/web_admin/migrations/0001_initial.py b/web_admin/migrations/0001_initial.py new file mode 100644 index 0000000..ad25a50 --- /dev/null +++ b/web_admin/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.2 on 2023-07-14 13:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_published', models.BooleanField(default=True)), + ('is_deleted', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='ArticleSurvey', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', models.CharField(max_length=255)), + ('points', models.IntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_survey', to='web_admin.article')), + ], + ), + migrations.CreateModel( + name='SurveyOption', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('option', models.CharField(max_length=255)), + ('is_answer', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='survey_options', to='web_admin.articlesurvey')), + ], + ), + migrations.CreateModel( + name='ArticleCard', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('image', models.ImageField(upload_to='card_images/')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_cards', to='web_admin.article')), + ], + ), + ] diff --git a/web_admin/migrations/__init__.py b/web_admin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web_admin/models.py b/web_admin/models.py new file mode 100644 index 0000000..1e49c6b --- /dev/null +++ b/web_admin/models.py @@ -0,0 +1,67 @@ +""" +web_admin model file +""" +# django imports +from django.db import models + + +class Article(models.Model): + """ + Article model + """ + title = models.CharField(max_length=255) + description = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_published = models.BooleanField(default=True) + is_deleted = models.BooleanField(default=False) + + def __str__(self): + """Return title""" + return f'{self.id} | {self.title}' + + +class ArticleCard(models.Model): + """ + Article Card model + """ + article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='article_cards') + title = models.CharField(max_length=255) + description = models.TextField() + image = models.ImageField(upload_to='card_images/') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + """Return title""" + return f'{self.id} | {self.title}' + + +class ArticleSurvey(models.Model): + """ + Article Survey model + """ + article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='article_survey') + question = models.CharField(max_length=255) + points = models.IntegerField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + """Return title""" + return f'{self.id} | {self.article}' + + +class SurveyOption(models.Model): + """ + Survey Options model + """ + survey = models.ForeignKey(ArticleSurvey, on_delete=models.CASCADE, related_name='survey_options') + option = models.CharField(max_length=255) + is_answer = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + """Return title""" + return f'{self.id} | {self.survey}' diff --git a/web_admin/permission.py b/web_admin/permission.py new file mode 100644 index 0000000..5ecf33a --- /dev/null +++ b/web_admin/permission.py @@ -0,0 +1,26 @@ +""" +web_admin permission classes +""" +# django imports +from rest_framework import permissions + + +class AdminPermission(permissions.BasePermission): + """ + to check for usertype admin only + """ + def has_permission(self, request, view): + """ + Return True if user_type is admin + """ + if request.user.is_superuser: + return True + return False + + def has_object_permission(self, request, view, obj): + """ + check for object level permission + """ + if request.user.is_superuser: + return True + return False diff --git a/web_admin/serializers.py b/web_admin/serializers.py new file mode 100644 index 0000000..57d4278 --- /dev/null +++ b/web_admin/serializers.py @@ -0,0 +1,162 @@ +""" +web_admin serializers file +""" +# django imports +from rest_framework import serializers +from django.conf import settings + +# local imports +from base.messages import ERROR_CODE +from web_admin.models import Article, ArticleCard, SurveyOption, ArticleSurvey +from web_admin.utils import pop_id + + +class ArticleCardSerializer(serializers.ModelSerializer): + """ + Article Card serializer + """ + id = serializers.IntegerField(required=False) + + class Meta: + """ + meta class + """ + model = ArticleCard + fields = ('id', 'title', 'description') + + +class SurveyOptionSerializer(serializers.ModelSerializer): + """ + survey option serializer + """ + id = serializers.IntegerField(required=False) + + class Meta: + """ + meta class + """ + model = SurveyOption + fields = ('id', 'option', 'is_answer') + + +class ArticleSurveySerializer(serializers.ModelSerializer): + """ + article survey serializer + """ + id = serializers.IntegerField(required=False) + survey_options = SurveyOptionSerializer(many=True) + + class Meta: + """ + meta class + """ + model = ArticleSurvey + fields = ('id', 'question', 'points', 'survey_options') + + +class ArticleSerializer(serializers.ModelSerializer): + """ + serializer for article API + """ + article_cards = ArticleCardSerializer(many=True) + article_survey = ArticleSurveySerializer(many=True) + + class Meta: + """ + meta class + """ + model = Article + fields = ('id', 'title', 'description', 'article_cards', 'article_survey') + + def validate(self, attrs): + """ + to validate request data + :param attrs: + :return: validated attrs + """ + article_cards = attrs.get('article_cards', None) + article_survey = attrs.get('article_survey', None) + if article_cards is None or len(article_cards) > int(settings.MAX_ARTICLE_CARD): + raise serializers.ValidationError({'details': ERROR_CODE['2039']}) + if article_survey is None or len(article_survey) < int(settings.MIN_ARTICLE_SURVEY) or int( + settings.MAX_ARTICLE_SURVEY) < len(article_survey): + raise serializers.ValidationError({'details': ERROR_CODE['2040']}) + return attrs + + def create(self, validated_data): + """ + to create article. + ID in post data dict is for update api. + :param validated_data: + :return: article object + """ + article_cards = validated_data.pop('article_cards') + article_survey = validated_data.pop('article_survey') + + article = Article.objects.create(**validated_data) + + for card in article_cards: + card = pop_id(card) + ArticleCard.objects.create(article=article, **card) + + for survey in article_survey: + survey = pop_id(survey) + options = survey.pop('survey_options') + survey_obj = ArticleSurvey.objects.create(article=article, **survey) + for option in options: + option = pop_id(option) + SurveyOption.objects.create(survey=survey_obj, **option) + + return article + + def update(self, instance, validated_data): + """ + to update article and related table + :param instance: + :param validated_data: + :return: article object + """ + article_cards = validated_data.pop('article_cards') + article_survey = validated_data.pop('article_survey') + instance.title = validated_data.get('title', instance.title) + instance.description = validated_data.get('description', instance.description) + instance.save() + + # Update or create cards + for card_data in article_cards: + card_id = card_data.get('id', None) + if card_id: + card = ArticleCard.objects.get(id=card_id, article=instance) + card.title = card_data.get('title', card.title) + card.description = card_data.get('description', card.description) + card.image = card_data.get('image', card.image) + card.save() + else: + card_data = pop_id(card_data) + ArticleCard.objects.create(article=instance, **card_data) + + # Update or create survey sections + for survey_data in article_survey: + survey_id = survey_data.get('id', None) + options_data = survey_data.pop('survey_options') + if survey_id: + survey = ArticleSurvey.objects.get(id=survey_id, article=instance) + survey.question = survey_data.get('question', survey.question) + survey.save() + else: + survey_data = pop_id(survey_data) + survey = ArticleSurvey.objects.create(article=instance, **survey_data) + + # Update or create survey options + for option_data in options_data: + option_id = option_data.get('id', None) + if option_id: + option = SurveyOption.objects.get(id=option_id, survey=survey) + option.option = option_data.get('option', option.option) + option.is_answer = option_data.get('is_answer', option.is_answer) + option.save() + else: + option_data = pop_id(option_data) + SurveyOption.objects.create(survey=survey, **option_data) + + return instance diff --git a/web_admin/tests.py b/web_admin/tests.py new file mode 100644 index 0000000..8800281 --- /dev/null +++ b/web_admin/tests.py @@ -0,0 +1,6 @@ +""" +web_admin test file +""" +from django.test import TestCase + +# Create your tests here. diff --git a/web_admin/urls.py b/web_admin/urls.py new file mode 100644 index 0000000..95bc7d9 --- /dev/null +++ b/web_admin/urls.py @@ -0,0 +1,18 @@ +""" +web_admin urls file +""" +# django imports +from django.urls import path, include +from rest_framework import routers + +# local imports +from web_admin.views import ArticleViewSet + +# initiate router +router = routers.SimpleRouter() + +router.register('article', ArticleViewSet, basename='article') + +urlpatterns = [ + path('api/v1/', include(router.urls)), +] diff --git a/web_admin/utils.py b/web_admin/utils.py new file mode 100644 index 0000000..2e09e2f --- /dev/null +++ b/web_admin/utils.py @@ -0,0 +1,13 @@ +""" +web_utils file +""" + + +def pop_id(data): + """ + to pop id, not in use + :param data: + :return: data + """ + data.pop('id') if 'id' in data else data + return data diff --git a/web_admin/views.py b/web_admin/views.py new file mode 100644 index 0000000..8003e2c --- /dev/null +++ b/web_admin/views.py @@ -0,0 +1,142 @@ +""" +web_admin views file +""" +# django imports +from rest_framework.viewsets import GenericViewSet, mixins +from rest_framework.response import Response +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated + +# local imports +from account.utils import custom_response, custom_error_response +from base.messages import SUCCESS_CODE, ERROR_CODE +from web_admin.models import Article, ArticleCard, ArticleSurvey +from web_admin.permission import AdminPermission +from web_admin.serializers import ArticleSerializer + + +class ArticleViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModelMixin, + mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin): + """ + article api + """ + serializer_class = ArticleSerializer + permission_classes = [IsAuthenticated, AdminPermission] + queryset = Article.objects.prefetch_related('article_cards', + 'article_survey', + 'article_survey__survey_options').order_by('-created_at') + + http_method_names = ['get', 'post', 'put', 'delete'] + + def create(self, request, *args, **kwargs): + """ + article create api method + :param request: + :param args: + :param kwargs: + :return: success message + """ + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return custom_response(SUCCESS_CODE["3027"]) + + def update(self, request, *args, **kwargs): + """ + article update api method + :param request: + :param args: + :param kwargs: + :return: success message + """ + article = self.queryset.filter(id=kwargs['pk']).first() + serializer = self.serializer_class(article, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return custom_response(SUCCESS_CODE["3028"]) + + def list(self, request, *args, **kwargs): + """ + article list api method + :param request: + :param args: + :param kwargs: + :return: list of article + """ + queryset = self.queryset.filter(is_deleted=False) + paginator = self.pagination_class() + paginated_queryset = paginator.paginate_queryset(queryset, request) + serializer = self.serializer_class(paginated_queryset, many=True) + return custom_response(None, data=serializer.data) + + def retrieve(self, request, *args, **kwargs): + """ + article detail api method + :param request: + :param args: + :param kwargs: + :return: article detail data + """ + queryset = self.queryset.filter(id=kwargs['pk'], is_deleted=False) + serializer = self.serializer_class(queryset, many=True) + return custom_response(None, data=serializer.data) + + def destroy(self, request, *args, **kwargs): + """ + article delete (soft delete) api method + :param request: + :param args: + :param kwargs: + :return: success message + """ + article = self.queryset.filter(id=kwargs['pk']).update(is_deleted=True) + if article: + return custom_response(SUCCESS_CODE["3029"]) + return custom_error_response(ERROR_CODE["2041"], status.HTTP_400_BAD_REQUEST) + + @action(methods=['get'], url_name='', url_path='', detail=False) + def search_article(self, request): + """ + article search api method + :param request: + :return: searched article + """ + search = request.GET.get('search') + queryset = self.queryset.filter(title__icontains=search) + paginator = self.pagination_class() + paginated_queryset = paginator.paginate_queryset(queryset, request) + serializer = self.serializer_class(paginated_queryset, many=True) + return custom_response(None, data=serializer.data) + + @action(methods=['get'], url_name='remove_card', url_path='remove_card', + detail=True, serializer_class=None) + def remove_article_card(self, request, *args, **kwargs): + """ + article card remove (delete) api method + :param request: + :param args: + :param kwargs: + :return: success message + """ + try: + ArticleCard.objects.filter(id=kwargs['pk']).first().delete() + return custom_response(SUCCESS_CODE["3030"]) + except AttributeError: + return custom_error_response(ERROR_CODE["2042"], response_status=status.HTTP_400_BAD_REQUEST) + + @action(methods=['get'], url_name='remove_survey', url_path='remove_survey', + detail=True, serializer_class=None) + def remove_article_survey(self, request, *args, **kwargs): + """ + article survey remove (delete) api method + :param request: + :param args: + :param kwargs: + :return: success message + """ + try: + ArticleSurvey.objects.filter(id=kwargs['pk']).first().delete() + return custom_response(SUCCESS_CODE["3031"]) + except AttributeError: + return custom_error_response(ERROR_CODE["2043"], response_status=status.HTTP_400_BAD_REQUEST) diff --git a/zod_bank/settings.py b/zod_bank/settings.py index c8b85a6..b107ffd 100644 --- a/zod_bank/settings.py +++ b/zod_bank/settings.py @@ -58,7 +58,9 @@ INSTALLED_APPS = [ 'django_ses', 'account', 'junior', - 'guardian' + 'guardian', + 'web_admin', + # 'social_django' ] # define middle ware here MIDDLEWARE = [ @@ -231,8 +233,14 @@ ALIYUN_OSS_BUCKET_NAME = os.getenv('ALIYUN_OSS_BUCKET_NAME') ALIYUN_OSS_ENDPOINT = os.getenv('ALIYUN_OSS_ENDPOINT') ALIYUN_OSS_REGION = os.getenv('ALIYUN_OSS_REGION') +MAX_ARTICLE_CARD = os.getenv('MAX_ARTICLE_CARD', 6) +MIN_ARTICLE_SURVEY = os.getenv('MIN_ARTICLE_SURVEY', 5) +MAX_ARTICLE_SURVEY = os.getenv('MAX_ARTICLE_SURVEY', 10) + # define static url STATIC_URL = 'static/' # define static root STATIC_ROOT = 'static' +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'media') diff --git a/zod_bank/urls.py b/zod_bank/urls.py index 274451e..009f1b3 100644 --- a/zod_bank/urls.py +++ b/zod_bank/urls.py @@ -30,4 +30,5 @@ urlpatterns = [ path('', include(('account.urls', 'account'), namespace='account')), path('', include('guardian.urls')), path('', include(('junior.urls', 'junior'), namespace='junior')), + path('', include(('web_admin.urls', 'web_admin'), namespace='web_admin')), ]