├── service_backend ├── apps │ ├── __init__.py │ ├── mail │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── tags │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── serializers.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── users │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0002_blacklist.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── serializers.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── utils │ │ ├── __init__.py │ │ ├── tests.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── filter.py │ │ ├── mail_helper.py │ │ ├── constants.py │ │ └── views.py │ ├── years │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0002_year_is_current.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── serializers.py │ │ ├── tests.py │ │ └── views.py │ ├── admins │ │ ├── __init__.py │ │ ├── serializers.py │ │ ├── models.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── tests.py │ │ └── views.py │ ├── chapters │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── serializers.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── images │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── apps.py │ │ └── views.py │ ├── issues │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0002_auto_20230420_0206.py │ │ │ ├── 0003_auto_20230423_1651.py │ │ │ ├── 0006_auto_20230522_2132.py │ │ │ ├── 0005_issueassociations.py │ │ │ ├── 0007_issueapicall.py │ │ │ ├── 0004_userdraft.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── serializer_comment.py │ │ ├── urls.py │ │ ├── views_issue_draft.py │ │ ├── views_issue_association.py │ │ ├── views_issue_comment.py │ │ ├── models.py │ │ ├── serializer_issue.py │ │ ├── views_issue_status.py │ │ └── tests.py │ ├── subjects │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── serializers.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ └── notifications │ │ ├── __init__.py │ │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_alter_notificationreceiver_table.py │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── models.py │ │ ├── views.py │ │ └── tests.py ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── README.md ├── .gitignore ├── uwsgi.ini ├── manage.py └── .github └── workflows ├── dev.yml └── prod.yml /service_backend/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # service-backend 2 | 3 | -------------------------------------------------------------------------------- /service_backend/apps/mail/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/tags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/years/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/admins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/admins/serializers.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/images/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/issues/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/mail/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/tags/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/years/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service_backend/apps/images/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /service_backend/apps/admins/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/images/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/images/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/utils/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/admins/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/issues/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/mail/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/tags/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/utils/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/years/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *__pycache__/ 3 | refresh_db.sh 4 | env.json 5 | db.sqlite3 6 | settings_copy.py 7 | static 8 | keyword -------------------------------------------------------------------------------- /service_backend/__init__.py: -------------------------------------------------------------------------------- 1 | from service_backend.settings import ENV 2 | if ENV['USE_MYSQL']: 3 | import pymysql 4 | pymysql.install_as_MySQLdb() 5 | -------------------------------------------------------------------------------- /service_backend/apps/images/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.images import views 3 | 4 | urlpatterns = [ 5 | path('upload/', views.UploadImage.as_view()), 6 | ] 7 | -------------------------------------------------------------------------------- /service_backend/apps/mail/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MailConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.mail' 7 | -------------------------------------------------------------------------------- /service_backend/apps/tags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TagsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.tags' 7 | -------------------------------------------------------------------------------- /service_backend/apps/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.users' 7 | -------------------------------------------------------------------------------- /service_backend/apps/years/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class YearConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.years' 7 | -------------------------------------------------------------------------------- /service_backend/apps/admins/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AdminsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.admins' 7 | -------------------------------------------------------------------------------- /service_backend/apps/images/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ImagesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.images' 7 | -------------------------------------------------------------------------------- /service_backend/apps/issues/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class IssuesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.issues' 7 | -------------------------------------------------------------------------------- /service_backend/apps/utils/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UtilsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.utils' 7 | 8 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChaptersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.chapters' 7 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SubjectsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.subjects' 7 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NotificationConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service_backend.apps.notifications' 7 | -------------------------------------------------------------------------------- /service_backend/apps/mail/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from service_backend.apps.mail import views 4 | 5 | urlpatterns = [ 6 | path('send', views.SendMail.as_view()), 7 | path('confirm', views.ConfirmMail.as_view()) 8 | ] 9 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket=127.0.0.1:8081 3 | chdir=/home/ubuntu/ShieAsk/release/service-backend-master 4 | wsgi-file=service_backend/wsgi.py 5 | processes=4 6 | threads=100 7 | chmod-socket = 666 8 | master=True 9 | pidfile=uwsgi.pid 10 | daemonize=uwsgi.log 11 | -------------------------------------------------------------------------------- /service_backend/apps/utils/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Create your models here. 5 | class MyModel(models.Model): 6 | created_at = models.DateTimeField(auto_now_add=True) 7 | updated_at = models.DateTimeField(auto_now=True) 8 | 9 | class Meta: 10 | abstract = True 11 | -------------------------------------------------------------------------------- /service_backend/apps/tags/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.tags import views 3 | 4 | urlpatterns = [ 5 | path('', views.TagList.as_view()), 6 | path('create', views.TagCreate.as_view()), 7 | path('update', views.TagUpdate.as_view()), 8 | path('delete', views.TagDelete.as_view()) 9 | ] 10 | -------------------------------------------------------------------------------- /service_backend/apps/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from service_backend.apps.users.models import User 3 | 4 | 5 | class UserSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = User 8 | fields = ['id', 'student_id', 'name', 'password_digest', 'mail', 'avatar', 'frozen', 'user_role'] 9 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.chapters import views 3 | 4 | urlpatterns = [ 5 | path('', views.ChapterList.as_view()), 6 | path('create', views.ChapterCreate.as_view()), 7 | path('update', views.ChapterUpdate.as_view()), 8 | path('delete', views.ChapterDelete.as_view()) 9 | ] 10 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.subjects import views 3 | 4 | urlpatterns = [ 5 | path('', views.SubjectList.as_view()), 6 | path('create', views.SubjectCreate.as_view()), 7 | path('update', views.SubjectUpdate.as_view()), 8 | path('delete', views.SubjectDelete.as_view()) 9 | ] 10 | -------------------------------------------------------------------------------- /service_backend/apps/years/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from service_backend.apps.utils.models import MyModel 4 | 5 | 6 | # Create your models here. 7 | class Year(MyModel): 8 | content = models.CharField(max_length=255, unique=True) 9 | is_current = models.BooleanField(default=False) 10 | 11 | class Meta: 12 | db_table = 'years' 13 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.notifications import views 3 | 4 | urlpatterns = [ 5 | path('clear_all', views.NotificationClear.as_view()), 6 | path('get', views.NotificationRead.as_view()), 7 | path('user_receive', views.NotificationList.as_view()), 8 | path('broadcast', views.NotificationBroadcast.as_view()), 9 | ] 10 | -------------------------------------------------------------------------------- /service_backend/apps/tags/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from service_backend.apps.tags.models import Tag 3 | 4 | 5 | class TagSerializer(serializers.ModelSerializer): 6 | tag_id = serializers.SerializerMethodField() 7 | 8 | class Meta: 9 | model = Tag 10 | fields = ['tag_id', 'content'] 11 | 12 | def get_tag_id(self, obj): 13 | return obj.id 14 | -------------------------------------------------------------------------------- /service_backend/apps/years/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.years import views 3 | 4 | urlpatterns = [ 5 | path('', views.YearList.as_view()), 6 | path('create', views.YearCreate.as_view()), 7 | path('update', views.YearUpdate.as_view()), 8 | path('delete', views.YearDelete.as_view()), 9 | path('update_current', views.YearCurrentUpdate.as_view()) 10 | ] 11 | -------------------------------------------------------------------------------- /service_backend/apps/years/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from service_backend.apps.years.models import Year 3 | 4 | 5 | class YearSerializer(serializers.ModelSerializer): 6 | year_id = serializers.SerializerMethodField() 7 | 8 | class Meta: 9 | model = Year 10 | fields = ['year_id', 'content'] 11 | 12 | def get_year_id(self, obj): 13 | return obj.id 14 | -------------------------------------------------------------------------------- /service_backend/apps/mail/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from service_backend.apps.users.models import User 4 | from service_backend.apps.utils.models import MyModel 5 | 6 | 7 | # Create your models here. 8 | 9 | class MailConfirm(MyModel): 10 | email = models.CharField(max_length=255) 11 | vcode = models.CharField(max_length=255) 12 | 13 | class Meta: 14 | db_table = 'mail_confirms' 15 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from service_backend.apps.chapters.models import Chapter 4 | 5 | 6 | class ChapterSerializer(serializers.ModelSerializer): 7 | chapter_id = serializers.SerializerMethodField() 8 | 9 | class Meta: 10 | model = Chapter 11 | fields = ['chapter_id', 'name', 'content'] 12 | 13 | def get_chapter_id(self, obj): 14 | return obj.id 15 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from service_backend.apps.subjects.models import Subject 4 | 5 | 6 | class SubjectSerializer(serializers.ModelSerializer): 7 | subject_id = serializers.SerializerMethodField() 8 | 9 | class Meta: 10 | model = Subject 11 | fields = ['subject_id', 'name', 'content'] 12 | 13 | def get_subject_id(self, obj): 14 | return obj.id 15 | -------------------------------------------------------------------------------- /service_backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for service_backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'service_backend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /service_backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for service_backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'service_backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/migrations/0002_alter_notificationreceiver_table.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-22 22:41 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('notifications', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelTable( 14 | name='notificationreceiver', 15 | table='notification_receivers', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /service_backend/apps/years/migrations/0002_year_is_current.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-30 14:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('years', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='year', 15 | name='is_current', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from service_backend.apps.utils.models import MyModel 4 | from service_backend.apps.subjects.models import Subject 5 | 6 | 7 | # Create your models here. 8 | class Chapter(MyModel): 9 | name = models.CharField(max_length=255) 10 | subject = models.ForeignKey(Subject, on_delete=models.CASCADE, related_name='chapters') 11 | content = models.CharField(max_length=3071) 12 | 13 | class Meta: 14 | db_table = 'chapters' 15 | -------------------------------------------------------------------------------- /service_backend/apps/tags/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from service_backend.apps.utils.models import MyModel 4 | from service_backend.apps.issues.models import Issue 5 | 6 | # Create your models here. 7 | class Tag(MyModel): 8 | content = models.CharField(max_length=255) 9 | 10 | class Meta: 11 | db_table = 'tags' 12 | 13 | 14 | class IssueTag(MyModel): 15 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='issue_tags') 16 | tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name='issue_tags') 17 | 18 | class Meta: 19 | db_table = 'issue_tags' 20 | -------------------------------------------------------------------------------- /service_backend/apps/admins/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.admins import views 3 | 4 | urlpatterns = [ 5 | path('update_privilege', views.UpdateUserRole.as_view()), 6 | path('users', views.UserList.as_view()), 7 | path('freeze_user', views.FreezeUser.as_view()), 8 | path('issue/delete', views.DeleteIssue.as_view()), 9 | path('create_user', views.CreateUser.as_view()), 10 | path('create_user_batch', views.CreateUserBatch.as_view()), 11 | path('statistics', views.GetStatistics.as_view()), 12 | path('tutor_bonus', views.GetTutorBonus.as_view()), 13 | path('student_bonus', views.GetStudentBonus.as_view()), 14 | ] 15 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/0002_auto_20230420_0206.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-20 02:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('issues', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='issue', 15 | name='follows', 16 | field=models.IntegerField(default=0), 17 | ), 18 | migrations.AddField( 19 | model_name='issue', 20 | name='likes', 21 | field=models.IntegerField(default=0), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/0003_auto_20230423_1651.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-23 16:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('issues', '0002_auto_20230420_0206'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='reviewissues', 15 | old_name='reviewer', 16 | new_name='reviewed', 17 | ), 18 | migrations.AlterField( 19 | model_name='reviewissues', 20 | name='status', 21 | field=models.IntegerField(null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/0006_auto_20230522_2132.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-22 21:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('issues', '0005_issueassociations'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='issue', 15 | name='counsel_at', 16 | field=models.DateTimeField(null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='issue', 20 | name='review_at', 21 | field=models.DateTimeField(null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /service_backend/apps/utils/filter.py: -------------------------------------------------------------------------------- 1 | class Filter: 2 | def __init__(self): 3 | self.keywords = [] 4 | self.parse() 5 | 6 | def parse(self, path="keyword"): 7 | try: 8 | f = open(path, 'r', encoding='utf-8') 9 | except Exception as e: 10 | return 11 | for keyword in f: 12 | self.keywords.append(keyword.strip()) 13 | 14 | def has_sensitive_word(self, message): 15 | for kw in self.keywords: 16 | if kw in message: 17 | return True 18 | return False 19 | 20 | # def filter(self, message, replace="*"): 21 | # for kw in self.keywords: 22 | # message = message.replace(kw, replace * len(kw)) 23 | # return message 24 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'service_backend.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | # init_db.init_database() 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /service_backend/apps/users/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from service_backend.apps.utils.models import MyModel 4 | 5 | 6 | # Create your models here. 7 | class User(MyModel): 8 | student_id = models.CharField(max_length=255, unique=True, db_index=True) 9 | name = models.CharField(max_length=255, db_index=True) 10 | password_digest = models.CharField(max_length=255) 11 | mail = models.EmailField() 12 | avatar = models.CharField(max_length=255) 13 | frozen = models.IntegerField(default=0) 14 | user_role = models.IntegerField(default=0) 15 | 16 | class Meta: 17 | db_table = 'users' 18 | 19 | 20 | class BlackList(MyModel): 21 | token = models.CharField(max_length=255, unique=True) 22 | 23 | class Meta: 24 | db_table = 'black_list' 25 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Pull and Rerun Backend Service 2 | # You may pin to the exact commit or the version. 3 | # uses: TencentCloudBase/cloudbase-action@7d4cf39abbdc53bad003a47c18d968c8a28916a2 4 | 5 | # 触发此CI/CD的事件 6 | on: 7 | push: # push时 8 | branches: # 选定push到哪个分支会触发该事件 9 | - dev # 只在push到master分支时触发 10 | 11 | jobs: 12 | # build job 13 | build: 14 | runs-on: ubuntu-20.04 15 | steps: 16 | - name: Checkout # 这一step的名称为Checkout 17 | uses: actions/checkout@master 18 | - name: Deploy 19 | uses: appleboy/ssh-action@master 20 | with: 21 | host: ${{ secrets.CLOUD_HOST}} 22 | username: ${{ secrets.CLOUD_USERNAME }} 23 | password: ${{ secrets.CLOUD_PASSWORD }} 24 | port: ${{ secrets.CLOUD_PORT }} 25 | script: | 26 | cd ~ 27 | source backend_cicd.sh -------------------------------------------------------------------------------- /.github/workflows/prod.yml: -------------------------------------------------------------------------------- 1 | name: Pull and Rerun Backend Service 2 | # You may pin to the exact commit or the version. 3 | # uses: TencentCloudBase/cloudbase-action@7d4cf39abbdc53bad003a47c18d968c8a28916a2 4 | 5 | # 触发此CI/CD的事件 6 | on: 7 | push: # push时 8 | branches: # 选定push到哪个分支会触发该事件 9 | - master # 只在push到master分支时触发 10 | 11 | jobs: 12 | # build job 13 | build: 14 | runs-on: ubuntu-20.04 15 | steps: 16 | - name: Checkout # 这一step的名称为Checkout 17 | uses: actions/checkout@master 18 | - name: Deploy 19 | uses: appleboy/ssh-action@master 20 | with: 21 | host: ${{ secrets.CLOUD_HOST}} 22 | username: ${{ secrets.CLOUD_USERNAME }} 23 | password: ${{ secrets.CLOUD_PASSWORD }} 24 | port: ${{ secrets.CLOUD_PORT }} 25 | script: | 26 | cd ~ 27 | source backend_cicd_prod.sh -------------------------------------------------------------------------------- /service_backend/apps/years/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-19 17:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Year', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_at', models.DateTimeField(auto_now_add=True)), 19 | ('updated_at', models.DateTimeField(auto_now=True)), 20 | ('content', models.CharField(max_length=255, unique=True)), 21 | ], 22 | options={ 23 | 'db_table': 'years', 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | from service_backend.apps.users.models import User 5 | from service_backend.apps.utils.models import MyModel 6 | 7 | 8 | class Notification(MyModel): 9 | title = models.CharField(max_length=255) 10 | content = models.CharField(max_length=255) 11 | category = models.IntegerField(default=0) 12 | 13 | class Meta: 14 | db_table = 'notifications' 15 | 16 | 17 | class NotificationReceiver(MyModel): 18 | notification = models.ForeignKey(Notification, on_delete=models.CASCADE, related_name='notification_receivers') 19 | receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_notifications') 20 | status = models.IntegerField(default=0) 21 | 22 | class Meta: 23 | db_table = 'notification_receivers' 24 | 25 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from service_backend.apps.utils.models import MyModel 4 | from service_backend.apps.years.models import Year 5 | from service_backend.apps.users.models import User 6 | 7 | 8 | # Create your models here. 9 | class Subject(MyModel): 10 | name = models.CharField(max_length=255, unique=True, db_index=True) 11 | year = models.ForeignKey(Year, on_delete=models.CASCADE, related_name='subjects') 12 | content = models.CharField(max_length=3071) 13 | 14 | class Meta: 15 | db_table = 'subjects' 16 | 17 | 18 | class UserSubject(MyModel): 19 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_subjects') 20 | subject = models.ForeignKey(Subject, on_delete=models.CASCADE, related_name='user_subjects') 21 | 22 | class Meta: 23 | db_table = 'user_subjects' 24 | -------------------------------------------------------------------------------- /service_backend/apps/users/migrations/0002_blacklist.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-12 15:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='BlackList', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('created_at', models.DateTimeField(auto_now_add=True)), 18 | ('updated_at', models.DateTimeField(auto_now=True)), 19 | ('token', models.CharField(max_length=255, unique=True)), 20 | ], 21 | options={ 22 | 'db_table': 'black_list', 23 | }, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /service_backend/apps/mail/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-22 16:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='MailConfirm', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_at', models.DateTimeField(auto_now_add=True)), 19 | ('updated_at', models.DateTimeField(auto_now=True)), 20 | ('email', models.CharField(max_length=255)), 21 | ('vcode', models.CharField(max_length=255)), 22 | ], 23 | options={ 24 | 'db_table': 'mail_confirms', 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /service_backend/apps/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from service_backend.apps.users import views 3 | 4 | urlpatterns = [ 5 | path('user_login', views.UserLogin.as_view()), 6 | path('user_logout', views.UserLogout.as_view()), 7 | path('password_modify', views.PasswordModify.as_view()), 8 | 9 | path('modify_user_info', views.ModifyUserInfo.as_view()), 10 | path('get_user_info', views.GetUserInfo.as_view()), 11 | 12 | path('get_user_subject', views.GetUserSubject.as_view()), 13 | path('modify_user_subject', views.ModifyUserSubject.as_view()), 14 | path('check_user_subject', views.CheckUserSubject.as_view()), 15 | 16 | path('get_review_issue', views.GetReviewIssue.as_view()), 17 | path('get_adopt_issue', views.GetAdoptIssue.as_view()), 18 | path('get_ask_issue', views.GetAskIssue.as_view()), 19 | path('get_follow_issue', views.GetFollowIssue.as_view()), 20 | 21 | path('active_users', views.GetActiveUser.as_view()), 22 | path('get_popular_issue', views.GetPopularIssue.as_view()), 23 | ] 24 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-19 17:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('subjects', '__first__'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Chapter', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ('name', models.CharField(max_length=255)), 23 | ('content', models.CharField(max_length=3071)), 24 | ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chapters', to='subjects.subject')), 25 | ], 26 | options={ 27 | 'db_table': 'chapters', 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/0005_issueassociations.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-22 18:45 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('issues', '0004_userdraft'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='IssueAssociations', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_at', models.DateTimeField(auto_now_add=True)), 19 | ('updated_at', models.DateTimeField(auto_now=True)), 20 | ('associate_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='associate_issues_as_to', to='issues.issue')), 21 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='associate_issues_as_from', to='issues.issue')), 22 | ], 23 | options={ 24 | 'db_table': 'issue_associations', 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/0007_issueapicall.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-22 22:41 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users', '0002_blacklist'), 11 | ('issues', '0006_auto_20230522_2132'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='IssueApiCall', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ('updated_at', models.DateTimeField(auto_now=True)), 21 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_api_call_history', to='issues.issue')), 22 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_api_call_history', to='users.user')), 23 | ], 24 | options={ 25 | 'db_table': 'issue_api_calls', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /service_backend/apps/mail/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test import TestCase 4 | 5 | from rest_framework.test import APITestCase 6 | 7 | from service_backend.apps.users.models import User 8 | from service_backend.apps.utils.views import encode_password 9 | 10 | 11 | # Create your tests here. 12 | class MailAPITestCase(APITestCase): 13 | def setUp(self): 14 | user = User(student_id='20373228', name='yyh', password_digest=encode_password('123456'), user_role=2, frozen=0) 15 | user.save() 16 | 17 | def test_send_mail(self): 18 | url = '/mail/send' 19 | data = { 20 | "mail": "1047813474@qq.com" 21 | } 22 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 23 | self.assertEqual(response.status_code, 200) 24 | return 25 | 26 | def test_confirm_mail(self): 27 | url = '/mail/confirm' 28 | data = { 29 | "student_id": 20373228, 30 | "mail": "20020706@buaa.edu.cn", 31 | "v_code": 1234, 32 | "password": 123456 33 | } 34 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 35 | self.assertEqual(response.data['code'], 1501) 36 | return 37 | -------------------------------------------------------------------------------- /service_backend/apps/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-19 17:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='User', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_at', models.DateTimeField(auto_now_add=True)), 19 | ('updated_at', models.DateTimeField(auto_now=True)), 20 | ('student_id', models.CharField(db_index=True, max_length=255, unique=True)), 21 | ('name', models.CharField(db_index=True, max_length=255)), 22 | ('password_digest', models.CharField(max_length=255)), 23 | ('mail', models.EmailField(max_length=254)), 24 | ('avatar', models.CharField(max_length=255)), 25 | ('frozen', models.IntegerField(default=0)), 26 | ('user_role', models.IntegerField(default=0)), 27 | ], 28 | options={ 29 | 'db_table': 'users', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/0004_userdraft.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-16 10:34 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('chapters', '0001_initial'), 11 | ('users', '0002_blacklist'), 12 | ('issues', '0003_auto_20230423_1651'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='UserDraft', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ('title', models.CharField(max_length=255, null=True)), 23 | ('content', models.CharField(max_length=3071, null=True)), 24 | ('anonymous', models.IntegerField(null=True)), 25 | ('chapter', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='draft_chapter', to='chapters.chapter')), 26 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_draft', to='users.user')), 27 | ], 28 | options={ 29 | 'db_table': 'user_drafts', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /service_backend/urls.py: -------------------------------------------------------------------------------- 1 | """service_backend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('user/', include('service_backend.apps.users.urls')), 22 | path('issue/', include('service_backend.apps.issues.urls')), 23 | path('subject/', include('service_backend.apps.subjects.urls')), 24 | path('chapter/', include('service_backend.apps.chapters.urls')), 25 | path('tag/', include('service_backend.apps.tags.urls')), 26 | path('year/', include('service_backend.apps.years.urls')), 27 | path('admins/', include('service_backend.apps.admins.urls')), 28 | path('images/', include('service_backend.apps.images.urls')), 29 | path('mail/', include('service_backend.apps.mail.urls')), 30 | path('notification/', include('service_backend.apps.notifications.urls')) 31 | ] 32 | -------------------------------------------------------------------------------- /service_backend/apps/issues/serializer_comment.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from service_backend.apps.issues.models import Comment 3 | 4 | 5 | class CommentSerializer(serializers.ModelSerializer): 6 | comment_id = serializers.SerializerMethodField() 7 | time = serializers.SerializerMethodField() 8 | user_id = serializers.SerializerMethodField() 9 | user_role = serializers.SerializerMethodField() 10 | avatar = serializers.SerializerMethodField() 11 | name = serializers.SerializerMethodField() 12 | 13 | def get_comment_id(self, obj): 14 | return obj.id 15 | 16 | def get_time(self, obj): 17 | return obj.updated_at.strftime('%Y-%m-%d %H:%M:%S') 18 | 19 | def get_user_id(self, obj): 20 | if obj.issue.user_id == obj.user_id and \ 21 | obj.issue.anonymous == 1: 22 | return 0 23 | else: 24 | return obj.user.id 25 | 26 | def get_user_role(self, obj): 27 | return obj.user.user_role 28 | 29 | def get_avatar(self, obj): 30 | if obj.issue.user_id == obj.user_id and \ 31 | obj.issue.anonymous == 1: 32 | return None 33 | else: 34 | return obj.user.avatar 35 | 36 | def get_name(self, obj): 37 | if obj.issue.user_id == obj.user_id and \ 38 | obj.issue.anonymous == 1: 39 | return "匿名" 40 | else: 41 | return obj.user.name 42 | 43 | class Meta: 44 | model = Comment 45 | fields = ['comment_id', 'content', 'time', 'user_id', 'user_role', 'avatar', 'name'] 46 | -------------------------------------------------------------------------------- /service_backend/apps/tags/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-19 17:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('issues', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Tag', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ('content', models.CharField(max_length=255)), 23 | ], 24 | options={ 25 | 'db_table': 'tags', 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='IssueTag', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('created_at', models.DateTimeField(auto_now_add=True)), 33 | ('updated_at', models.DateTimeField(auto_now=True)), 34 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_tags', to='issues.issue')), 35 | ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_tags', to='tags.tag')), 36 | ], 37 | options={ 38 | 'db_table': 'issue_tags', 39 | }, 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-16 10:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('users', '0002_blacklist'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Notification', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ('title', models.CharField(max_length=255)), 23 | ('content', models.CharField(max_length=255)), 24 | ('category', models.IntegerField(default=0)), 25 | ], 26 | options={ 27 | 'db_table': 'notifications', 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='NotificationReceiver', 32 | fields=[ 33 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('created_at', models.DateTimeField(auto_now_add=True)), 35 | ('updated_at', models.DateTimeField(auto_now=True)), 36 | ('status', models.IntegerField(default=0)), 37 | ('notification', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_receivers', to='notifications.notification')), 38 | ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_notifications', to='users.user')), 39 | ], 40 | options={ 41 | 'abstract': False, 42 | }, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /service_backend/apps/tags/tests.py: -------------------------------------------------------------------------------- 1 | from service_backend.apps.subjects.models import Subject 2 | from rest_framework.test import APITestCase 3 | 4 | from service_backend.apps.tags.models import Tag 5 | from service_backend.apps.years.models import Year 6 | from service_backend.apps.years.serializers import YearSerializer 7 | 8 | 9 | # Create your tests here. 10 | class SubjectAPITestCase(APITestCase): 11 | def setUp(self): 12 | tag = Tag(id=1, content="tag_1") 13 | tag.save() 14 | tag = Tag(id=2, content="tag_2") 15 | tag.save() 16 | return 17 | 18 | def test_list(self): 19 | url = '/tag/' 20 | response = self.client.get(url) 21 | self.assertEqual(response.status_code, 200) 22 | self.assertEqual(len(response.data['data']['tag_list']), 2) 23 | return 24 | 25 | def test_create(self): 26 | url = '/tag/create' 27 | data = { 28 | "content": "create_content" 29 | } 30 | response = self.client.post(url, data) 31 | self.assertEqual(response.status_code, 200) 32 | self.assertEqual(response.data['success'], True) 33 | self.assertEqual(response.data['message'], 'create tag success!') 34 | return 35 | 36 | def test_update(self): 37 | url = '/tag/update' 38 | data = { 39 | "tag_id": 1, 40 | "content": "update_content_test" 41 | } 42 | response = self.client.post(url, data) 43 | self.assertEqual(response.status_code, 200) 44 | self.assertEqual(response.data['message'], "update tag success!") 45 | return 46 | 47 | def test_delete(self): 48 | url = '/tag/delete' 49 | data = { 50 | "tag_id": 2 51 | } 52 | response = self.client.delete(url, data) 53 | self.assertEqual(response.status_code, 200) 54 | self.assertEqual(response.data['message'], "delete tag success!") 55 | return 56 | -------------------------------------------------------------------------------- /service_backend/apps/issues/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from service_backend.apps.issues import views_issue, views_issue_comment, views_issue_status, views_issue_draft, \ 4 | views_issue_association 5 | 6 | urlpatterns = [ 7 | path('get', views_issue.IssueGet.as_view()), 8 | path('follow_check', views_issue.IssueFollowCheck.as_view()), 9 | path('follow', views_issue.IssueFollow.as_view()), 10 | path('favorite', views_issue.IssueFavorite.as_view()), 11 | path('like', views_issue.IssueLike.as_view()), 12 | path('update', views_issue.IssueUpdate.as_view()), 13 | path('commit', views_issue.IssueCommit.as_view()), 14 | path('tags', views_issue.IssueTagList.as_view()), 15 | path('tags_update', views_issue.IssueTagListUpdate.as_view()), 16 | path('', views_issue.IssueSearch.as_view()), 17 | 18 | path('cancel', views_issue_status.IssueCancel.as_view()), 19 | path('classify', views_issue_status.IssueClassify.as_view()), 20 | path('readopt', views_issue_status.IssueReadopt.as_view()), 21 | path('review', views_issue_status.IssueReview.as_view()), 22 | path('agree', views_issue_status.IssueAgree.as_view()), 23 | path('reject', views_issue_status.IssueReject.as_view()), 24 | path('adopt', views_issue_status.IssueAdopt.as_view()), 25 | 26 | path('comments', views_issue_comment.CommentList.as_view()), 27 | path('comment', views_issue_comment.CommentDelete.as_view()), 28 | path('comment/update', views_issue_comment.CommentUpdate.as_view()), 29 | path('comment/create', views_issue_comment.CommentCreate.as_view()), 30 | 31 | path('save_draft', views_issue_draft.SaveDraft.as_view()), 32 | path('load_draft', views_issue_draft.LoadDraft.as_view()), 33 | 34 | path('associate', views_issue_association.AddAssociation.as_view()), 35 | path('associate/get', views_issue_association.GetAssociation.as_view()), 36 | path('associate/delete', views_issue_association.DeleteAssociation.as_view()) 37 | ] 38 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-19 17:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('years', '__first__'), 13 | ('users', '__first__'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Subject', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('created_at', models.DateTimeField(auto_now_add=True)), 22 | ('updated_at', models.DateTimeField(auto_now=True)), 23 | ('name', models.CharField(db_index=True, max_length=255, unique=True)), 24 | ('content', models.CharField(max_length=3071)), 25 | ('year', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subjects', to='years.year')), 26 | ], 27 | options={ 28 | 'db_table': 'subjects', 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='UserSubject', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('created_at', models.DateTimeField(auto_now_add=True)), 36 | ('updated_at', models.DateTimeField(auto_now=True)), 37 | ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_subjects', to='subjects.subject')), 38 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_subjects', to='users.user')), 39 | ], 40 | options={ 41 | 'db_table': 'user_subjects', 42 | }, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /service_backend/apps/utils/mail_helper.py: -------------------------------------------------------------------------------- 1 | import random 2 | import re 3 | import smtplib 4 | from email.header import Header 5 | from email.mime.text import MIMEText 6 | from email.utils import parseaddr, formataddr 7 | 8 | 9 | def _format_addr(s): 10 | name, addr = parseaddr(s) 11 | return formataddr((Header(name, 'utf-8').encode(), addr)) 12 | 13 | 14 | def gen_vcode(length=6): 15 | return ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', k=length)) 16 | 17 | 18 | def is_valid(email) -> bool: 19 | regex = re.compile(r'([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+') 20 | if re.fullmatch(regex, email): 21 | return True 22 | else: 23 | return False 24 | 25 | 26 | def gen_vcode_msg(vcode, to_addr, from_addr='moss_se2023@163.com'): 27 | """ 28 | vcode: 发送的验证码 29 | from_addr: 发送方邮箱 30 | to_addr: 接收方邮箱 31 | return: 返回含有发送验证码的MIMEText对象 32 | """ 33 | text = f'您好,欢迎使用MOSS团队开发的ShieAsk平台。\n您的验证码是: {vcode}, 有效期为20分钟。' 34 | msg = MIMEText(text, 'plain', 'utf-8') 35 | msg['From'] = _format_addr('ShieAsk平台<%s>' % from_addr) 36 | msg['To'] = _format_addr('用户<%s>' % to_addr) 37 | msg['Subject'] = Header('ShieAsk平台验证码', 'utf-8').encode() 38 | return msg 39 | 40 | 41 | def send_vcode(to_addr, smtp_server='smtp.163.com', from_addr='moss_se2023@163.com', password='WEYVECFDPURIPBPJ'): 42 | """ 43 | smtp_server: 当前使用的smtp服务器 44 | from_addr: 发送方邮箱 45 | password: 发送方邮箱密码(smtp授权码) 46 | to_addr: 接收方邮箱 47 | """ 48 | server = smtplib.SMTP(smtp_server, 25) 49 | # server.set_debuglevel(1) 50 | server.login(from_addr, password) 51 | vcode = gen_vcode() 52 | msg = gen_vcode_msg(vcode, to_addr) 53 | server.sendmail(from_addr, to_addr, msg.as_string()) 54 | server.quit() 55 | return vcode 56 | 57 | # if __name__ == "__main__": 58 | # to = '20373228@buaa.edu.cn' 59 | # code = send_vcode(to) 60 | # print('发送的验证码:', code) 61 | # # 利用code进行相关操作 62 | -------------------------------------------------------------------------------- /service_backend/apps/years/tests.py: -------------------------------------------------------------------------------- 1 | from service_backend.apps.years.models import Year 2 | from rest_framework.test import APITestCase 3 | 4 | 5 | # Create your tests here. 6 | class YearAPITestCase(APITestCase): 7 | def setUp(self): 8 | year = Year(id=1, content='test1') 9 | year.save() 10 | year = Year(id=2, content='test2') 11 | year.save() 12 | 13 | def test_list(self): 14 | url = '/year/' 15 | response = self.client.get(url) 16 | self.assertEqual(response.status_code, 200) 17 | self.assertEqual(len(response.data['data']['year_list']), 2) 18 | return 19 | 20 | def test_create(self): 21 | url = '/year/create' 22 | data = {"content": "test_content"} 23 | response = self.client.post(url, data) 24 | self.assertEqual(response.status_code, 200) 25 | self.assertEqual(response.data['success'], True) 26 | self.assertEqual(response.data['code'], 0) 27 | self.assertEqual(response.data['message'], 'create year success!') 28 | return 29 | 30 | def test_update(self): 31 | url = '/year/update' 32 | data = { 33 | "year_id": 1, 34 | "content": "test_update" 35 | } 36 | response = self.client.post(url, data) 37 | self.assertEqual(response.status_code, 200) 38 | self.assertEqual(response.data['message'], "update year success!") 39 | return 40 | 41 | def test_delete(self): 42 | url = '/year/delete' 43 | data = { 44 | "year_id": 2 45 | } 46 | response = self.client.delete(url, data) 47 | self.assertEqual(response.status_code, 200) 48 | self.assertEqual(response.data['message'], "delete year success!") 49 | return 50 | 51 | def test_current_year(self): 52 | url = '/year/update_current' 53 | data = { 54 | "year_id": 1, 55 | "content": "当前学年" 56 | } 57 | response = self.client.post(url, data) 58 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /service_backend/apps/subjects/tests.py: -------------------------------------------------------------------------------- 1 | from service_backend.apps.subjects.models import Subject 2 | from rest_framework.test import APITestCase 3 | 4 | from service_backend.apps.years.models import Year 5 | 6 | 7 | # Create your tests here. 8 | class SubjectAPITestCase(APITestCase): 9 | def setUp(self): 10 | year = Year(id=1, content="year") 11 | year.save() 12 | subject = Subject(id=1, name='subject_1', content='content_1', year=year) 13 | subject.save() 14 | subject = Subject(id=2, name='subject_2', content='content_2', year=year) 15 | subject.save() 16 | return 17 | 18 | def test_list(self): 19 | url = '/subject/' 20 | data = { 21 | "year_id": 1 22 | } 23 | response = self.client.post(url, data=data) 24 | self.assertEqual(response.status_code, 200) 25 | self.assertEqual(len(response.data['data']['subject_list']), 2) 26 | return 27 | 28 | def test_create(self): 29 | url = '/subject/create' 30 | data = { 31 | "year_id": 1, 32 | "name": "subject_3", 33 | "content": "create_content" 34 | } 35 | response = self.client.post(url, data) 36 | self.assertEqual(response.status_code, 200) 37 | self.assertEqual(response.data['success'], True) 38 | self.assertEqual(response.data['message'], 'create subject success!') 39 | return 40 | 41 | def test_update(self): 42 | url = '/subject/update' 43 | data = { 44 | "subject_id": 1, 45 | "name": "update_content_test", 46 | "content": "update_content_test" 47 | } 48 | response = self.client.post(url, data) 49 | self.assertEqual(response.status_code, 200) 50 | self.assertEqual(response.data['message'], "update subject success!") 51 | return 52 | 53 | def test_delete(self): 54 | url = '/subject/delete' 55 | data = { 56 | "subject_id": 2 57 | } 58 | response = self.client.delete(url, data) 59 | self.assertEqual(response.status_code, 200) 60 | self.assertEqual(response.data['message'], "delete subject success!") 61 | return 62 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/tests.py: -------------------------------------------------------------------------------- 1 | from service_backend.apps.chapters.models import Chapter 2 | from rest_framework.test import APITestCase 3 | 4 | from service_backend.apps.subjects.models import Subject 5 | from service_backend.apps.years.models import Year 6 | 7 | 8 | # Create your tests here. 9 | class ChapterAPITestCase(APITestCase): 10 | def setUp(self): 11 | year = Year(id=1, content="year") 12 | year.save() 13 | subject = Subject(id=1, name='subject', content='content', year=year) 14 | subject.save() 15 | chapter = Chapter(id=1, name='chapter_1', content='content_1', subject=subject) 16 | chapter.save() 17 | chapter = Chapter(id=2, name='chapter_2', content='content_2', subject=subject) 18 | chapter.save() 19 | return 20 | 21 | def test_list(self): 22 | url = '/chapter/' 23 | data = { 24 | "subject_id": 1 25 | } 26 | response = self.client.post(url, data) 27 | self.assertEqual(response.status_code, 200) 28 | self.assertEqual(len(response.data['data']['chapter_list']), 2) 29 | return 30 | 31 | def test_create(self): 32 | url = '/chapter/create' 33 | data = { 34 | "subject_id": 1, 35 | "name": "chapter_3", 36 | "content": "create_content" 37 | } 38 | response = self.client.post(url, data) 39 | self.assertEqual(response.status_code, 200) 40 | self.assertEqual(response.data['success'], True) 41 | self.assertEqual(response.data['message'], 'create chapter success!') 42 | return 43 | 44 | def test_update(self): 45 | url = '/chapter/update' 46 | data = { 47 | "chapter_id": 1, 48 | "name": "update_content_test", 49 | "content": "update_content_test" 50 | } 51 | response = self.client.post(url, data) 52 | self.assertEqual(response.status_code, 200) 53 | self.assertEqual(response.data['message'], "update chapter success!") 54 | return 55 | 56 | def test_delete(self): 57 | url = '/chapter/delete' 58 | data = { 59 | "chapter_id": 2 60 | } 61 | response = self.client.delete(url, data) 62 | self.assertEqual(response.status_code, 200) 63 | self.assertEqual(response.data['message'], "delete chapter success!") 64 | return 65 | -------------------------------------------------------------------------------- /service_backend/apps/issues/views_issue_draft.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | 4 | from service_backend.apps.issues.models import UserDraft, Comment 5 | from service_backend.apps.utils.constants import UserRole, CommentErrorCode, DraftErrorCode 6 | from service_backend.apps.utils.views import check_role, response_json 7 | 8 | 9 | class SaveDraft(APIView): 10 | @check_role([UserRole.STUDENT, UserRole.TUTOR, UserRole.ADMIN, ]) 11 | def post(self, request, action_user): 12 | chapter_id = request.data['chapter_id'] if request.data['chapter_id'] else None 13 | title = request.data['title'] if request.data['title'] else None 14 | content = request.data['content'] if request.data['content'] else None 15 | anonymous = request.data['anonymous'] if request.data['anonymous'] else 0 16 | 17 | origin_draft = UserDraft.objects.filter(user=action_user) 18 | if origin_draft: 19 | origin_draft.delete() 20 | 21 | draft = UserDraft(user=action_user, chapter_id=chapter_id, title=title, content=content, anonymous=anonymous) 22 | try: 23 | draft.save() 24 | except Exception: 25 | return Response(response_json( 26 | success=False, 27 | code=DraftErrorCode.DRAFT_SAVE_FAILED, 28 | message="can't save draft!" 29 | ), status=404) 30 | return Response(response_json( 31 | success=True, 32 | message="save draft success!" 33 | )) 34 | 35 | 36 | class LoadDraft(APIView): 37 | @check_role([UserRole.STUDENT, UserRole.TUTOR, UserRole.ADMIN, ]) 38 | def post(self, request, action_user): 39 | user_draft = UserDraft.objects.filter(user=action_user) 40 | data = { 41 | "chapter_id": None, 42 | "title": None, 43 | "content": None, 44 | "anonymous": 0, 45 | "subject_id": None 46 | } 47 | 48 | if user_draft: 49 | data["chapter_id"] = user_draft.first().chapter_id 50 | data["title"] = user_draft.first().title 51 | data["content"] = user_draft.first().content 52 | data["anonymous"] = user_draft.first().anonymous 53 | if user_draft.first().chapter_id: 54 | data["subject_id"] = user_draft.first().chapter.subject_id 55 | 56 | return Response(response_json( 57 | success=True, 58 | message="load draft success!", 59 | data=data 60 | )) 61 | -------------------------------------------------------------------------------- /service_backend/apps/tags/views.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from rest_framework.response import Response 3 | 4 | from service_backend.apps.tags.models import Tag 5 | from rest_framework.views import APIView 6 | 7 | from service_backend.apps.utils.constants import TagErrorCode 8 | from service_backend.apps.utils.views import response_json 9 | from service_backend.apps.tags.serializers import TagSerializer 10 | 11 | 12 | # Create your views here. 13 | def _find_tag(): 14 | def decorated(func): 15 | @wraps(func) 16 | def wrapper(*args, **kwargs): 17 | try: 18 | tag = Tag.objects.get(id=args[1].data['tag_id']) 19 | except Exception: 20 | return Response(response_json( 21 | success=False, 22 | code=TagErrorCode.TAG_DOES_NOT_EXIST, 23 | message="can't find tag!" 24 | ), status=404) 25 | return func(*args, **kwargs, tag=tag) 26 | 27 | return wrapper 28 | 29 | return decorated 30 | 31 | 32 | class TagList(APIView): 33 | def get(self, request): 34 | tag = Tag.objects.all() 35 | tag_serializer = TagSerializer(tag, many=True) 36 | data = {'tag_list': tag_serializer.data} 37 | return Response(response_json( 38 | success=True, 39 | data=data 40 | )) 41 | 42 | 43 | class TagCreate(APIView): 44 | def post(self, request): 45 | content = request.data['content'] 46 | tag = Tag(content=content) 47 | try: 48 | tag.save() 49 | except Exception: 50 | return Response(response_json( 51 | success=False, 52 | code=TagErrorCode.TAG_SAVE_FAILED, 53 | message="can't save tag!" 54 | ), status=404) 55 | 56 | return Response(response_json( 57 | success=True, 58 | message="create tag success!" 59 | )) 60 | 61 | 62 | class TagUpdate(APIView): 63 | @_find_tag() 64 | def post(self, request, tag): 65 | tag.content = request.data['content'] 66 | try: 67 | tag.save() 68 | except Exception: 69 | return Response(response_json( 70 | success=False, 71 | code=TagErrorCode.TAG_SAVE_FAILED, 72 | message="can't update tag!" 73 | ), status=404) 74 | 75 | return Response(response_json( 76 | success=True, 77 | message="update tag success!" 78 | )) 79 | 80 | 81 | class TagDelete(APIView): 82 | @_find_tag() 83 | def delete(self, request, tag): 84 | try: 85 | tag.delete() 86 | except Exception: 87 | return Response(response_json( 88 | success=False, 89 | code=TagErrorCode.TAG_DELETE_FAILED, 90 | message="can't delete tag!" 91 | ), status=404) 92 | return Response(response_json( 93 | success=True, 94 | message="delete tag success!" 95 | )) 96 | -------------------------------------------------------------------------------- /service_backend/apps/images/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | from service_backend.settings import MEDIA_ROOT, STATIC_URL 4 | from service_backend.apps.users.models import User 5 | from service_backend.apps.utils.views import check_role, response_json 6 | from service_backend.apps.utils.constants import ImageErrorCode, UserRole 7 | 8 | import os 9 | import uuid 10 | import imghdr 11 | from datetime import datetime 12 | 13 | PIC_URL_BASE = "https://shieask.com/pic/" 14 | # PIC_URL_BASE = 'http://localhost:8000' 15 | 16 | 17 | # Create your views here. 18 | class UploadAvatar(APIView): 19 | 20 | def post(self, request, action_user=None): 21 | pass 22 | 23 | 24 | class UploadImage(APIView): 25 | 26 | @check_role(UserRole.ALL_USERS) 27 | def post(self, request, action_user: User = None): 28 | try: 29 | image = request.data['file'] 30 | image_type = imghdr.what(image) 31 | # print(type(image)) 32 | except Exception as _e: 33 | # raise _e 34 | return Response(response_json( 35 | success=False, 36 | code=ImageErrorCode.IMAGE_LOAD_FAILED, 37 | message="image load failed!" 38 | )) 39 | if image.size > (5 << 20): 40 | return Response(response_json( 41 | success=False, 42 | code=ImageErrorCode.IMAGE_TOO_BIG, 43 | message="size of uploaded image can't exceed 5MB!" 44 | )) 45 | if image_type is None: 46 | return Response(response_json( 47 | success=False, 48 | code=ImageErrorCode.INVALID_IMAGE_FORMAT, 49 | message="not an image-format file!" 50 | )) 51 | 52 | image_root = os.path.join(MEDIA_ROOT) 53 | # image_name = action_user.student_id + '_' + datetime.now().strftime("%Y%m%d%H%M%S") + '_' + str(uuid.uuid4()) + '.' + image_type 54 | image_name = datetime.now().strftime("%Y%m%d%H%M%S") + '_' + str(uuid.uuid4()) + '.' + image_type 55 | image_path = os.path.join(image_root, image_name) 56 | # get file 57 | img_suffix = image.name.split('.')[-1] 58 | if image_type != ('jpeg' if img_suffix == 'jpg' else img_suffix): 59 | return Response(response_json( 60 | success=False, 61 | code=ImageErrorCode.UNEXPECTED_IMAGE_NAME, 62 | message='wrong image suffix!' 63 | )) 64 | # store file 65 | try: 66 | print(image_root) 67 | if not os.path.exists(image_root): 68 | os.mkdir(image_root) 69 | with open(image_path, 'wb+') as f: 70 | for chunk in image.chunks(): 71 | f.write(chunk) 72 | f.close() 73 | print(PIC_URL_BASE + STATIC_URL) 74 | except Exception as _e: 75 | return Response(response_json( 76 | success=False, 77 | code=ImageErrorCode.IMAGE_SAVE_FAILED, 78 | message='image save failed!' 79 | )) 80 | return Response(response_json( 81 | success=True, 82 | message='upload image successfully!', 83 | data={ 84 | 'url': PIC_URL_BASE + image_name 85 | } 86 | )) 87 | -------------------------------------------------------------------------------- /service_backend/apps/mail/views.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | 6 | from service_backend.apps.mail.models import MailConfirm 7 | from service_backend.apps.users.models import User 8 | from service_backend.apps.utils.constants import MailErrorCode, UserErrorCode 9 | from service_backend.apps.utils.mail_helper import send_vcode, is_valid 10 | 11 | from service_backend.apps.utils.views import response_json, encode_password 12 | 13 | 14 | # Create your views here. 15 | class SendMail(APIView): 16 | def post(self, request): 17 | to_mail = request.data['mail'] 18 | vcode = send_vcode(to_mail) 19 | mail_confirm = MailConfirm.objects.filter(email=to_mail) 20 | if not is_valid(to_mail): 21 | return Response(response_json( 22 | success=False, 23 | code=MailErrorCode.MAIL_FORMAT_WRONG, 24 | message="mail format wrong!" 25 | ), status=404) 26 | 27 | if mail_confirm: 28 | mail_confirm = mail_confirm.first() 29 | mail_confirm.vcode = vcode 30 | else: 31 | mail_confirm = MailConfirm(email=to_mail, vcode=vcode) 32 | 33 | try: 34 | mail_confirm.save() 35 | except Exception: 36 | return Response(response_json( 37 | success=False, 38 | message="can't send mail!" 39 | ), status=404) 40 | 41 | return Response(response_json( 42 | success=True, 43 | message="mail send success!" 44 | )) 45 | 46 | 47 | class ConfirmMail(APIView): 48 | def post(self, request): 49 | student_id = request.data['student_id'] 50 | mail = request.data['mail'] 51 | vcode = request.data['v_code'] 52 | password = request.data['password'] 53 | 54 | user = User.objects.filter(student_id=student_id) 55 | if not user: 56 | return Response(response_json( 57 | success=False, 58 | code=UserErrorCode.USER_NOT_FOUND, 59 | message="user not found!" 60 | ), status=404) 61 | user = user.first() 62 | 63 | if mail != user.mail: 64 | return Response(response_json( 65 | success=False, 66 | code=MailErrorCode.MAIL_CONFIRM_FAILED, 67 | message="student_id doesn't match mail" 68 | ), status=404) 69 | 70 | mail_confirm = MailConfirm.objects.filter(email=mail, vcode=vcode) 71 | if not (mail_confirm and timezone.now() - mail_confirm.first().updated_at < timezone.timedelta(minutes=20)): 72 | return Response(response_json( 73 | success=False, 74 | code=MailErrorCode.MAIL_CONFIRM_FAILED, 75 | message="invalid vcode!" 76 | ), status=404) 77 | 78 | user.password_digest = encode_password(password) 79 | try: 80 | user.save() 81 | except Exception: 82 | return Response(response_json( 83 | success=False, 84 | code=UserErrorCode.USER_SAVE_FAILED, 85 | message="can't change password!" 86 | ), status=404) 87 | 88 | return Response(response_json( 89 | success=True, 90 | message="change password success!" 91 | )) 92 | -------------------------------------------------------------------------------- /service_backend/apps/utils/constants.py: -------------------------------------------------------------------------------- 1 | class GlobalCode: 2 | SUCCESS = 0 3 | SYSTEM_FAILED = 1 4 | 5 | 6 | class UserRole: 7 | ALL_USERS = [0, 1, 2] 8 | ADMIN_ONLY = [2, ] 9 | STUDENT = 0 10 | TUTOR = 1 11 | ADMIN = 2 12 | 13 | 14 | DEFAULT_AVATAR = "https://shieask.com/pic/default_avatar.png" 15 | 16 | 17 | # 问题状态,0:未认领(默认),1:已认领,2:未认领复审 3: 已认领复审 4: 有效提问 5: 无效提问 18 | class IssueStatus: 19 | NOT_ADOPT = 0 20 | ADOPTING = 1 21 | NOT_REVIEW = 2 22 | REVIEWING = 3 23 | VALID_ISSUE = 4 24 | INVALID_ISSUE = 5 25 | 26 | 27 | class NotificationCategory: 28 | GLOBAL = 0 29 | ISSUE = 1 30 | 31 | 32 | class YearErrorCode: 33 | YEAR_SAVE_FAILED = 101 34 | YEAR_DOES_NOT_EXIST = 102 35 | YEAR_DELETE_FAILED = 103 36 | 37 | 38 | class UserErrorCode: 39 | USER_NOT_FOUND = 201 40 | INCORRECT_PASSWORD = 202 41 | EXPIRED_JWT = 203 42 | INVALID_JWT = 204 43 | USER_SAVE_FAILED = 205 44 | USER_FROZEN = 206 45 | PERMISSION_DENIED = 207 46 | USER_LOAD_FAILED = 208 47 | 48 | 49 | class SubjectErrorCode: 50 | SUBJECT_SAVE_FAILED = 301 51 | SUBJECT_DOES_NOT_EXIST = 302 52 | SUBJECT_DELETE_FAILED = 303 53 | SUBJECT_LIST_UPDATE_FAILED = 304 54 | 55 | 56 | class ChapterErrorCode: 57 | CHAPTER_SAVE_FAILED = 401 58 | CHAPTER_DOES_NOT_EXIST = 402 59 | CHAPTER_DELETE_FAILED = 403 60 | 61 | 62 | class TagErrorCode: 63 | TAG_SAVE_FAILED = 501 64 | TAG_DOES_NOT_EXIST = 502 65 | TAG_DELETE_FAILED = 503 66 | 67 | 68 | class IssueErrorCode: 69 | ISSUE_NOT_FOUND = 601 70 | ISSUE_DELETE_FAILED = 602 71 | ISSUE_SAVED_FAILED = 603 72 | REVIEW_ISSUE_QUERY_FAILED = 604 73 | ADOPT_ISSUE_QUERY_FAILED = 605 74 | FOLLOW_ISSUE_QUERY_FAILED = 606 75 | ASK_ISSUE_QUERY_FAILED = 607 76 | ISSUE_ACTION_REJECT = 608 77 | ISSUE_RELATE_FAILED = 609 78 | 79 | 80 | class IssueLikeErrorCode: 81 | ISSUE_LIKE_SAVED_FAILED = 701 82 | ISSUE_LIKE_DELETE_FAILED = 702 83 | 84 | 85 | class IssueFollowErrorCode: 86 | ISSUE_FOLLOW_SAVED_FAILED = 701 87 | ISSUE_FOLLOW_DELETE_FAILED = 702 88 | 89 | 90 | class IssueTagErrorCode: 91 | ISSUE_TAG_SAVED_FAILED = 801 92 | ISSUE_TAG_DELETE_FAILED = 802 93 | 94 | 95 | class IssueReviewerErrorCode: 96 | REVIEWER_ISSUE_SAVED_FAILED = 901 97 | 98 | 99 | class OtherErrorCode: 100 | UNEXPECTED_JSON_FORMAT = 1001 101 | TOO_LARGE_TOPK = 1002 102 | 103 | 104 | class ImageErrorCode: 105 | IMAGE_LOAD_FAILED = 1101 106 | WRONG_IMAGE_FORMAT = 1102 107 | UNEXPECTED_IMAGE_NAME = 1103 108 | IMAGE_SAVE_FAILED = 1104 109 | INVALID_IMAGE_FORMAT = 1105 110 | IMAGE_TOO_BIG = 1106 111 | 112 | 113 | class CommentErrorCode: 114 | COMMENT_NOT_FOUND = 1201 115 | COMMENT_DELETE_FAILED = 1202 116 | COMMENT_SAVED_FAILED = 1203 117 | 118 | 119 | class DraftErrorCode: 120 | DRAFT_SAVE_FAILED = 1301 121 | DRAFT_LOAD_FAILED = 1302 122 | 123 | 124 | class NotificationErrorCode: 125 | NOTIFICATION_SAVE_FAILED = 1401 126 | NOTIFICATION_LOAD_FAILED = 1402 127 | NOTIFICATION_DELETE_FAILED = 1403 128 | NOTIFICATION_RECEIVER_SAVE_FAILED = 1404 129 | NOTIFICATION_RECEIVER_LOAD_FAILED = 1405 130 | 131 | 132 | class MailErrorCode: 133 | MAIL_CONFIRM_FAILED = 1501 134 | MAIL_FORMAT_WRONG = 1502 135 | 136 | 137 | class StatisticsErrorCode: 138 | BONUS_ALL_THE_SAME = 1601 139 | -------------------------------------------------------------------------------- /service_backend/apps/issues/views_issue_association.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | 4 | from service_backend.apps.issues.models import Issue, IssueAssociations 5 | from service_backend.apps.issues.serializer_issue import IssueSearchSerializer 6 | from service_backend.apps.issues.views_issue import find_issue, allow_relate 7 | from service_backend.apps.utils.constants import UserRole, IssueErrorCode 8 | from service_backend.apps.utils.views import check_role, response_json 9 | 10 | 11 | class AddAssociation(APIView): 12 | @find_issue() 13 | @check_role([UserRole.TUTOR, UserRole.ADMIN, ]) 14 | def post(self, request, issue, action_user): 15 | if allow_relate(issue, action_user) != 1: 16 | return Response(response_json( 17 | success=False, 18 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 19 | message="you have no access to this issue!" 20 | ), status=404) 21 | try: 22 | associate_issue = Issue.objects.get(id=request.data['issue_associate_id']) 23 | except Exception: 24 | return Response(response_json( 25 | success=False, 26 | code=IssueErrorCode.ISSUE_NOT_FOUND, 27 | message="can't find issue!" 28 | ), status=404) 29 | if IssueAssociations.objects.filter(issue=issue, associate_issue=associate_issue): 30 | return Response(response_json( 31 | success=False, 32 | code=IssueErrorCode.ISSUE_RELATE_FAILED, 33 | message="already relate!" 34 | )) 35 | 36 | association = IssueAssociations(issue=issue, associate_issue=associate_issue) 37 | try: 38 | association.save() 39 | except Exception: 40 | return Response(response_json( 41 | success=False, 42 | code=IssueErrorCode.ISSUE_RELATE_FAILED, 43 | message="add relate success!" 44 | )) 45 | return Response(response_json( 46 | success=True, 47 | message="add relate success!" 48 | )) 49 | 50 | 51 | class GetAssociation(APIView): 52 | @find_issue() 53 | @check_role([UserRole.STUDENT, UserRole.TUTOR, UserRole.ADMIN, ]) 54 | def post(self, request, issue, action_user): 55 | associate_issues = [i.associate_issue for i in issue.associate_issues_as_from.all()] 56 | issue_list = IssueSearchSerializer(associate_issues, many=True).data 57 | return Response(response_json( 58 | success=True, 59 | data={ 60 | "issue_list": issue_list 61 | } 62 | )) 63 | 64 | 65 | class DeleteAssociation(APIView): 66 | @find_issue() 67 | @check_role([UserRole.TUTOR, UserRole.ADMIN, ]) 68 | def post(self, request, issue, action_user): 69 | try: 70 | associate_issue = Issue.objects.get(id=request.data['issue_associate_id']) 71 | except Exception: 72 | return Response(response_json( 73 | success=False, 74 | code=IssueErrorCode.ISSUE_NOT_FOUND, 75 | message="can't find issue!" 76 | ), status=404) 77 | associations = IssueAssociations.objects.filter(issue=issue, associate_issue=associate_issue) 78 | if associations: 79 | associations.delete() 80 | return Response(response_json( 81 | success=True, 82 | message="delete association success!" 83 | )) 84 | -------------------------------------------------------------------------------- /service_backend/apps/chapters/views.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from rest_framework.response import Response 3 | 4 | from service_backend.apps.chapters.models import Chapter 5 | from rest_framework.views import APIView 6 | 7 | from service_backend.apps.subjects.models import Subject 8 | from service_backend.apps.utils.constants import ChapterErrorCode, SubjectErrorCode, UserRole 9 | from service_backend.apps.utils.views import response_json, check_role 10 | from service_backend.apps.chapters.serializers import ChapterSerializer 11 | 12 | 13 | # Create your views here. 14 | def find_chapter(): 15 | def decorated(func): 16 | @wraps(func) 17 | def wrapper(*args, **kwargs): 18 | try: 19 | chapter = Chapter.objects.get(id=args[1].data['chapter_id']) 20 | except Exception: 21 | return Response(response_json( 22 | success=False, 23 | code=ChapterErrorCode.CHAPTER_DOES_NOT_EXIST, 24 | message="can't find chapter!" 25 | ), status=404) 26 | return func(*args, **kwargs, chapter=chapter) 27 | 28 | return wrapper 29 | 30 | return decorated 31 | 32 | 33 | class ChapterList(APIView): 34 | def post(self, request): 35 | try: 36 | subject = Subject.objects.get(id=request.data['subject_id']) 37 | except Exception: 38 | return Response(response_json( 39 | success=False, 40 | code=SubjectErrorCode.SUBJECT_DOES_NOT_EXIST, 41 | message="can't find subject!" 42 | ), status=404) 43 | chapter_serializer = ChapterSerializer(subject.chapters, many=True) 44 | data = {'chapter_list': chapter_serializer.data} 45 | return Response(response_json( 46 | success=True, 47 | data=data 48 | )) 49 | 50 | 51 | class ChapterCreate(APIView): 52 | def post(self, request): 53 | try: 54 | subject = Subject.objects.get(id=request.data['subject_id']) 55 | except Exception: 56 | return Response(response_json( 57 | success=False, 58 | code=SubjectErrorCode.SUBJECT_DOES_NOT_EXIST, 59 | message="can't find subject!" 60 | ), status=404) 61 | name = request.data['name'] 62 | content = request.data['content'] 63 | chapter = Chapter(name=name, content=content, subject=subject) 64 | try: 65 | chapter.save() 66 | except Exception: 67 | return Response(response_json( 68 | success=False, 69 | code=ChapterErrorCode.CHAPTER_SAVE_FAILED, 70 | message="can't save chapter!" 71 | ), status=404) 72 | 73 | return Response(response_json( 74 | success=True, 75 | message="create chapter success!" 76 | )) 77 | 78 | 79 | class ChapterUpdate(APIView): 80 | @find_chapter() 81 | def post(self, request, chapter): 82 | chapter.name = request.data['name'] 83 | chapter.content = request.data['content'] 84 | try: 85 | chapter.save() 86 | except Exception: 87 | return Response(response_json( 88 | success=False, 89 | code=ChapterErrorCode.CHAPTER_SAVE_FAILED, 90 | message="can't update chapter!" 91 | ), status=404) 92 | 93 | return Response(response_json( 94 | success=True, 95 | message="update chapter success!" 96 | )) 97 | 98 | 99 | class ChapterDelete(APIView): 100 | @find_chapter() 101 | def delete(self, request, chapter): 102 | try: 103 | chapter.delete() 104 | except Exception: 105 | return Response(response_json( 106 | success=False, 107 | code=ChapterErrorCode.CHAPTER_DELETE_FAILED, 108 | message="can't delete chapter!" 109 | ), status=404) 110 | return Response(response_json( 111 | success=True, 112 | message="delete chapter success!" 113 | )) 114 | -------------------------------------------------------------------------------- /service_backend/apps/issues/views_issue_comment.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | 4 | from service_backend.apps.issues.models import Comment 5 | from service_backend.apps.notifications.models import Notification, NotificationReceiver 6 | from service_backend.apps.users.models import User 7 | from service_backend.apps.utils.constants import UserRole, CommentErrorCode, NotificationErrorCode, NotificationCategory 8 | from service_backend.apps.utils.views import response_json, check_role 9 | from service_backend.apps.issues.serializer_comment import CommentSerializer 10 | from service_backend.apps.issues.views_issue import find_issue, find_comment 11 | 12 | 13 | class CommentList(APIView): 14 | @find_issue() 15 | def post(self, request, issue): 16 | commet_serializer = CommentSerializer(issue.comments, many=True) 17 | data = {"comment_list": commet_serializer.data} 18 | return Response(response_json( 19 | success=True, 20 | data=data 21 | )) 22 | 23 | 24 | class CommentCreate(APIView): 25 | @check_role([UserRole.STUDENT, UserRole.TUTOR, UserRole.ADMIN, ]) 26 | @find_issue() 27 | def post(self, request, issue, action_user: User): 28 | content = request.data['content'] 29 | comment = Comment(content=content, issue=issue, user=action_user) 30 | try: 31 | comment.save() 32 | except Exception: 33 | return Response(response_json( 34 | success=False, 35 | code=CommentErrorCode.COMMENT_SAVED_FAILED, 36 | message="can't save comment! probably have sensitive word!" 37 | ), status=404) 38 | 39 | if action_user.user_role == UserRole.TUTOR: 40 | try: 41 | notification = Notification( 42 | title="【issue回复通知】", 43 | content="你的问题:\"{}\" 有辅导师的新回答~".format(issue.title), 44 | category=NotificationCategory.ISSUE 45 | ) 46 | notification.save() 47 | except Exception: 48 | return Response(response_json( 49 | success=False, 50 | code=NotificationErrorCode.NOTIFICATION_SAVE_FAILED, 51 | message="can't save notification!" 52 | )) 53 | 54 | try: 55 | notification_receive = NotificationReceiver(notification=notification, receiver=issue.user) 56 | notification_receive.save() 57 | except Exception: 58 | return Response(response_json( 59 | success=True, 60 | code=NotificationErrorCode.NOTIFICATION_RECEIVER_SAVE_FAILED, 61 | message="can't save notification-receiver" 62 | )) 63 | 64 | return Response(response_json( 65 | success=True, 66 | message="create comment success!", 67 | data={"comment_id": comment.id} 68 | )) 69 | 70 | 71 | class CommentUpdate(APIView): 72 | @find_comment() 73 | def post(self, request, comment): 74 | comment.content = request.data['content'] 75 | try: 76 | comment.save() 77 | except Exception: 78 | return Response(response_json( 79 | success=False, 80 | code=CommentErrorCode.COMMENT_SAVED_FAILED, 81 | message="can't save comment! probably have sensitive word!" 82 | ), status=404) 83 | 84 | return Response(response_json( 85 | success=True, 86 | message="update comment success!" 87 | )) 88 | 89 | 90 | class CommentDelete(APIView): 91 | @find_comment() 92 | def delete(self, request, comment): 93 | try: 94 | comment.delete() 95 | except Exception: 96 | return Response(response_json( 97 | success=False, 98 | code=CommentErrorCode.COMMENT_DELETE_FAILED, 99 | message="can't delete comment!" 100 | ), status=404) 101 | 102 | return Response(response_json( 103 | success=True, 104 | message="delete comment success!" 105 | )) 106 | -------------------------------------------------------------------------------- /service_backend/apps/years/views.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | from service_backend.apps.years.models import Year 6 | from service_backend.apps.utils.views import response_json 7 | from service_backend.apps.years.serializers import YearSerializer 8 | from service_backend.apps.utils.constants import YearErrorCode 9 | 10 | 11 | # Create your views here. 12 | def _find_year(): 13 | def decorated(func): 14 | @wraps(func) 15 | def wrapper(*args, **kwargs): 16 | try: 17 | year = Year.objects.get(id=args[1].data['year_id']) 18 | except Exception: 19 | return Response(response_json( 20 | success=False, 21 | code=YearErrorCode.YEAR_DOES_NOT_EXIST, 22 | message="can't find year!" 23 | ), status=404) 24 | return func(*args, **kwargs, year=year) 25 | 26 | return wrapper 27 | 28 | return decorated 29 | 30 | 31 | class YearList(APIView): 32 | def get(self, request): 33 | year = Year.objects.all() 34 | year_serializer = YearSerializer(year, many=True) 35 | data = { 36 | "year_list": year_serializer.data, 37 | "current_year_id": 0, 38 | "current_content": "" 39 | } 40 | 41 | current_year = Year.objects.filter(is_current=True) 42 | if current_year: 43 | data["current_year_id"] = current_year.first().id 44 | data['current_content'] = current_year.first().content 45 | 46 | return Response(response_json( 47 | success=True, 48 | data=data 49 | )) 50 | 51 | 52 | class YearCreate(APIView): 53 | def post(self, request): 54 | year = Year(content=request.data['content']) 55 | try: 56 | year.save() 57 | except Exception: 58 | return Response(response_json( 59 | success=False, 60 | code=YearErrorCode.YEAR_SAVE_FAILED, 61 | message="can't save year!" 62 | ), status=404) 63 | 64 | return Response(response_json( 65 | success=True, 66 | message="create year success!" 67 | )) 68 | 69 | 70 | class YearUpdate(APIView): 71 | @_find_year() 72 | def post(self, request, year=None): 73 | year.content = request.data['content'] 74 | try: 75 | year.save() 76 | except Exception: 77 | return Response(response_json( 78 | success=False, 79 | code=YearErrorCode.YEAR_SAVE_FAILED, 80 | message="can't update year!" 81 | ), status=404) 82 | return Response(response_json( 83 | success=True, 84 | message="update year success!" 85 | )) 86 | 87 | 88 | class YearDelete(APIView): 89 | @_find_year() 90 | def delete(self, request, year): 91 | try: 92 | year.delete() 93 | except Exception as _e: 94 | return Response(response_json( 95 | success=False, 96 | code=YearErrorCode.YEAR_DELETE_FAILED, 97 | message="can't delete year!" 98 | ), status=404) 99 | return Response(response_json( 100 | success=True, 101 | message="delete year success!" 102 | )) 103 | 104 | 105 | class YearCurrentUpdate(APIView): 106 | @_find_year() 107 | def post(self, request, year): 108 | try: 109 | current_year = Year.objects.filter(is_current=True) 110 | if current_year: 111 | current_year.first().is_current = False 112 | current_year.first().save() 113 | year.is_current = True 114 | year.save() 115 | except Exception: 116 | return Response(response_json( 117 | success=False, 118 | code=YearErrorCode.YEAR_SAVE_FAILED, 119 | message="can't set this year as current_year" 120 | )) 121 | return Response(response_json( 122 | success=True, 123 | message="change current year success!" 124 | )) 125 | -------------------------------------------------------------------------------- /service_backend/apps/subjects/views.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from rest_framework.response import Response 3 | 4 | from service_backend.apps.subjects.models import Subject 5 | from service_backend.apps.chapters.models import Chapter 6 | from rest_framework.views import APIView 7 | 8 | from service_backend.apps.utils.constants import SubjectErrorCode, YearErrorCode 9 | from service_backend.apps.utils.views import response_json 10 | from service_backend.apps.subjects.serializers import SubjectSerializer 11 | from service_backend.apps.years.models import Year 12 | 13 | 14 | # Create your views here. 15 | def _find_subject(): 16 | def decorated(func): 17 | @wraps(func) 18 | def wrapper(*args, **kwargs): 19 | try: 20 | subject = Subject.objects.get(id=args[1].data['subject_id']) 21 | except Exception: 22 | return Response(response_json( 23 | success=False, 24 | code=SubjectErrorCode.SUBJECT_DOES_NOT_EXIST, 25 | message="can't find subject!" 26 | ), status=404) 27 | return func(*args, **kwargs, subject=subject) 28 | 29 | return wrapper 30 | 31 | return decorated 32 | 33 | 34 | class SubjectList(APIView): 35 | def post(self, request): 36 | try: 37 | year = Year.objects.get(id=request.data['year_id']) 38 | except Exception: 39 | return Response(response_json( 40 | success=False, 41 | code=YearErrorCode.YEAR_DOES_NOT_EXIST, 42 | message="can't find year!" 43 | ), status=404) 44 | subject_serializer = SubjectSerializer(year.subjects, many=True) 45 | data = {'subject_list': subject_serializer.data} 46 | return Response(response_json( 47 | success=True, 48 | data=data 49 | )) 50 | 51 | 52 | class SubjectCreate(APIView): 53 | def post(self, request): 54 | name = request.data['name'] 55 | content = request.data['content'] 56 | try: 57 | year = Year.objects.get(id=request.data['year_id']) 58 | except Exception: 59 | return Response(response_json( 60 | success=False, 61 | code=YearErrorCode.YEAR_DOES_NOT_EXIST, 62 | message="can't find year!" 63 | ), status=404) 64 | 65 | subject = Subject(name=name, content=content, year=year) 66 | try: 67 | subject.save() 68 | except Exception: 69 | return Response(response_json( 70 | success=False, 71 | code=SubjectErrorCode.SUBJECT_SAVE_FAILED, 72 | message="can't save subject!" 73 | ), status=404) 74 | chapter = Chapter(name='未分类', content='该目录下存放未分类章节的问题。', subject=subject) 75 | try: 76 | chapter.save() 77 | except Exception: 78 | return Response(response_json( 79 | success=False, 80 | code=ChapterErrorCode.CHAPTER_SAVE_FAILED, 81 | message="can't save chapter!" 82 | ), status=404) 83 | return Response(response_json( 84 | success=True, 85 | message="create subject success!" 86 | )) 87 | 88 | 89 | class SubjectUpdate(APIView): 90 | @_find_subject() 91 | def post(self, request, subject): 92 | subject.name = request.data['name'] 93 | subject.content = request.data['content'] 94 | try: 95 | subject.save() 96 | except Exception: 97 | return Response(response_json( 98 | success=False, 99 | code=SubjectErrorCode.SUBJECT_SAVE_FAILED, 100 | message="can't update subject!" 101 | ), status=404) 102 | 103 | return Response(response_json( 104 | success=True, 105 | message="update subject success!" 106 | )) 107 | 108 | 109 | class SubjectDelete(APIView): 110 | @_find_subject() 111 | def delete(self, request, subject): 112 | try: 113 | subject.delete() 114 | except Exception: 115 | return Response(response_json( 116 | success=False, 117 | code=SubjectErrorCode.SUBJECT_DELETE_FAILED, 118 | message="can't delete subject!" 119 | ), status=404) 120 | return Response(response_json( 121 | success=True, 122 | message="delete subject success!" 123 | )) 124 | -------------------------------------------------------------------------------- /service_backend/apps/issues/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from service_backend.apps.utils.filter import Filter 4 | from service_backend.apps.utils.models import MyModel 5 | from service_backend.apps.users.models import User 6 | from service_backend.apps.chapters.models import Chapter 7 | 8 | 9 | # Create your models here. 10 | class Issue(MyModel): 11 | title = models.CharField(max_length=255) 12 | content = models.CharField(max_length=3071) 13 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_issues') 14 | chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE, related_name='issues') 15 | counselor = models.ForeignKey(User, on_delete=models.CASCADE, related_name='counselor_issues', null=True) 16 | reviewer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reviewer_issues', null=True) 17 | counsel_at = models.DateTimeField(null=True) 18 | review_at = models.DateTimeField(null=True) 19 | status = models.IntegerField() 20 | anonymous = models.IntegerField() 21 | score = models.IntegerField(null=True) 22 | likes = models.IntegerField(default=0) 23 | follows = models.IntegerField(default=0) 24 | 25 | def save(self, *args, **kwargs): 26 | # filter 27 | flt = Filter() 28 | if flt.has_sensitive_word(self.title) or flt.has_sensitive_word(self.content): 29 | raise Exception 30 | return super(Issue, self).save(*args, **kwargs) 31 | 32 | class Meta: 33 | db_table = 'issues' 34 | 35 | 36 | class Comment(MyModel): 37 | content = models.CharField(max_length=3071) 38 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='comments') 39 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments') 40 | 41 | def save(self, *args, **kwargs): 42 | # filter 43 | flt = Filter() 44 | if flt.has_sensitive_word(self.content): 45 | return False 46 | return super(Comment, self).save(*args, **kwargs) 47 | 48 | class Meta: 49 | db_table = 'comments' 50 | 51 | 52 | class FollowIssues(MyModel): 53 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='follow_issues') 54 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='follow_issues') 55 | 56 | class Meta: 57 | db_table = 'follow_issues' 58 | 59 | 60 | class LikeIssues(MyModel): 61 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='like_issues') 62 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='like_issues') 63 | 64 | class Meta: 65 | db_table = 'like_issues' 66 | 67 | 68 | class AdoptIssues(MyModel): 69 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='adopt_issues') 70 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='adopt_issues') 71 | status = models.IntegerField() 72 | 73 | class Meta: 74 | db_table = 'adopt_issues' 75 | 76 | 77 | class ReviewIssues(MyModel): 78 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='review_issues') 79 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_review_issues') 80 | reviewed = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reviewer_review_issues') 81 | status = models.IntegerField(null=True) 82 | 83 | class Meta: 84 | db_table = 'review_issues' 85 | 86 | 87 | class UserDraft(MyModel): 88 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_draft') 89 | title = models.CharField(max_length=255, null=True) 90 | content = models.CharField(max_length=3071, null=True) 91 | chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE, related_name='draft_chapter', null=True) 92 | anonymous = models.IntegerField(null=True) 93 | 94 | class Meta: 95 | db_table = 'user_drafts' 96 | 97 | 98 | class IssueAssociations(MyModel): 99 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='associate_issues_as_from') 100 | associate_issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='associate_issues_as_to') 101 | 102 | class Meta: 103 | db_table = 'issue_associations' 104 | 105 | 106 | class IssueApiCall(MyModel): 107 | issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='issue_api_call_history') 108 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='issue_api_call_history') 109 | 110 | class Meta: 111 | db_table = 'issue_api_calls' 112 | -------------------------------------------------------------------------------- /service_backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for service_backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.18. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import json 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | with open('env.json') as env: 23 | ENV = json.load(env) 24 | 25 | SECRET_KEY = ENV['SECRET_KEY'] 26 | 27 | # settings.py 28 | if ENV.get('env') == 'dev': 29 | DEBUG = True 30 | else: 31 | DEBUG = False 32 | 33 | ALLOWED_HOSTS = ['*'] 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'django_extensions', 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 'rest_framework', 46 | 'service_backend.apps.utils', 47 | 'service_backend.apps.admins', 48 | 'service_backend.apps.chapters', 49 | 'service_backend.apps.images', 50 | 'service_backend.apps.issues', 51 | 'service_backend.apps.subjects', 52 | 'service_backend.apps.tags', 53 | 'service_backend.apps.users', 54 | 'service_backend.apps.years', 55 | 'service_backend.apps.mail', 56 | 'service_backend.apps.notifications' 57 | ] 58 | 59 | MIDDLEWARE = [ 60 | 'django.middleware.security.SecurityMiddleware', 61 | 'django.contrib.sessions.middleware.SessionMiddleware', 62 | 'django.middleware.common.CommonMiddleware', 63 | 'django.middleware.csrf.CsrfViewMiddleware', 64 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 65 | 'django.contrib.messages.middleware.MessageMiddleware', 66 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 67 | ] 68 | 69 | ROOT_URLCONF = 'service_backend.urls' 70 | 71 | TEMPLATES = [ 72 | { 73 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 74 | 'DIRS': [], 75 | 'APP_DIRS': True, 76 | 'OPTIONS': { 77 | 'context_processors': [ 78 | 'django.template.context_processors.debug', 79 | 'django.template.context_processors.request', 80 | 'django.contrib.auth.context_processors.auth', 81 | 'django.contrib.messages.context_processors.messages', 82 | ], 83 | }, 84 | }, 85 | ] 86 | 87 | WSGI_APPLICATION = 'service_backend.wsgi.application' 88 | 89 | # Database 90 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 91 | 92 | if not ENV['USE_MYSQL']: 93 | DATABASES = { 94 | 'default': { 95 | 'ENGINE': 'django.db.backends.sqlite3', 96 | 'NAME': BASE_DIR / 'db.sqlite3', 97 | } 98 | } 99 | else: 100 | DATABASES = { 101 | 'default': { 102 | 'ENGINE': ENV['DATABASES']['ENGINE'], 103 | 'NAME': ENV['DATABASES']['NAME'], 104 | 'USER': ENV['DATABASES']['USER'], 105 | 'PASSWORD': ENV['DATABASES']['PASSWORD'], 106 | 'HOST': ENV['DATABASES']['HOST'], 107 | 'PORT': ENV['DATABASES']['PORT'] 108 | } 109 | } 110 | 111 | # Password validation 112 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 113 | 114 | AUTH_PASSWORD_VALIDATORS = [ 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 117 | }, 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 120 | }, 121 | { 122 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 123 | }, 124 | { 125 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 126 | }, 127 | ] 128 | 129 | # Internationalization 130 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 131 | 132 | LANGUAGE_CODE = 'zh-hans' 133 | 134 | TIME_ZONE = 'Asia/Shanghai' 135 | 136 | USE_I18N = True 137 | 138 | USE_L10N = True 139 | 140 | USE_TZ = False 141 | 142 | # Static files (CSS, JavaScript, Images) 143 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 144 | 145 | STATIC_URL = '/static/' 146 | 147 | # media file saved path for browser 148 | MEDIA_ROOT = '/home/ubuntu/pic' 149 | # MEDIA_ROOT = os.path.join(BASE_DIR, 'static') 150 | 151 | # static file find path 152 | STATICFILES_DIRS = [ 153 | MEDIA_ROOT 154 | ] 155 | 156 | # Default primary key field type 157 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 158 | 159 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 160 | -------------------------------------------------------------------------------- /service_backend/apps/issues/serializer_issue.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from service_backend.apps.issues.models import Issue 3 | 4 | 5 | class IssueSerializer(serializers.ModelSerializer): 6 | issue_id = serializers.SerializerMethodField() 7 | user_id = serializers.SerializerMethodField() 8 | user_name = serializers.SerializerMethodField() 9 | user_avatar = serializers.SerializerMethodField() 10 | chapter_id = serializers.SerializerMethodField() 11 | chapter_name = serializers.SerializerMethodField() 12 | subject_id = serializers.SerializerMethodField() 13 | subject_name = serializers.SerializerMethodField() 14 | tag_list = serializers.SerializerMethodField() 15 | created_at = serializers.SerializerMethodField() 16 | updated_at = serializers.SerializerMethodField() 17 | 18 | def get_issue_id(self, obj): 19 | return obj.id 20 | 21 | def get_user_id(self, obj): 22 | if obj.anonymous: 23 | return 0 24 | else: 25 | return obj.user.id 26 | 27 | def get_user_name(self, obj): 28 | if obj.anonymous: 29 | return "匿名" 30 | else: 31 | return obj.user.name 32 | 33 | def get_user_avatar(self, obj): 34 | if obj.anonymous: 35 | return None 36 | else: 37 | return obj.user.avatar 38 | 39 | def get_chapter_id(self, obj): 40 | return obj.chapter.id 41 | 42 | def get_chapter_name(self, obj): 43 | return obj.chapter.name 44 | 45 | def get_subject_id(self, obj): 46 | return obj.chapter.subject.id 47 | 48 | def get_subject_name(self, obj): 49 | return obj.chapter.subject.name 50 | 51 | def get_created_at(self, obj): 52 | return obj.created_at.strftime('%Y-%m-%d %H:%M:%S') 53 | 54 | def get_updated_at(self, obj): 55 | return obj.updated_at.strftime('%Y-%m-%d %H:%M:%S') 56 | 57 | def get_tag_list(self, obj): 58 | issue_tags = obj.issue_tags.all() 59 | tags_content = [issue_tag.tag.content for issue_tag in issue_tags] 60 | return tags_content 61 | 62 | class Meta: 63 | model = Issue 64 | fields = ['issue_id', 'title', 'content', 'user_id', 'user_name', 'user_avatar', 'chapter_id', 'chapter_name', 65 | 'subject_id', 'subject_name', 'tag_list', 'status', 'anonymous', 'score', 'created_at', 'updated_at'] 66 | 67 | 68 | class IssueSearchSerializer(serializers.ModelSerializer): 69 | issue_id = serializers.SerializerMethodField() 70 | user_id = serializers.SerializerMethodField() 71 | user_name = serializers.SerializerMethodField() 72 | user_avatar = serializers.SerializerMethodField() 73 | chapter_id = serializers.SerializerMethodField() 74 | chapter_name = serializers.SerializerMethodField() 75 | subject_id = serializers.SerializerMethodField() 76 | subject_name = serializers.SerializerMethodField() 77 | issue_title = serializers.SerializerMethodField() 78 | like_count = serializers.SerializerMethodField() 79 | follow_count = serializers.SerializerMethodField() 80 | created_at = serializers.SerializerMethodField() 81 | updated_at = serializers.SerializerMethodField() 82 | tag_name_list = serializers.SerializerMethodField() 83 | 84 | def get_issue_id(self, obj): 85 | return obj.id 86 | 87 | def get_issue_title(self, obj): 88 | return obj.title 89 | 90 | def get_user_id(self, obj): 91 | if obj.anonymous == 1: 92 | return 0 93 | else: 94 | return obj.user.id 95 | 96 | def get_user_name(self, obj): 97 | if obj.anonymous == 1: 98 | return "匿名" 99 | else: 100 | return obj.user.name 101 | 102 | def get_user_avatar(self, obj): 103 | if obj.anonymous == 1: 104 | return None 105 | else: 106 | return obj.user.avatar 107 | 108 | def get_chapter_id(self, obj): 109 | return obj.chapter.id 110 | 111 | def get_chapter_name(self, obj): 112 | return obj.chapter.name 113 | 114 | def get_subject_id(self, obj): 115 | return obj.chapter.subject.id 116 | 117 | def get_subject_name(self, obj): 118 | return obj.chapter.subject.name 119 | 120 | def get_counselor_id(self, obj): 121 | return obj.counselor.id 122 | 123 | def get_reviewer_id(self, obj): 124 | return obj.reviewer.id 125 | 126 | def get_like_count(self, obj): 127 | return obj.likes 128 | 129 | def get_follow_count(self, obj): 130 | return obj.follows 131 | 132 | def get_created_at(self, obj): 133 | return obj.created_at.strftime('%Y-%m-%d %H:%M:%S') 134 | 135 | def get_updated_at(self, obj): 136 | return obj.updated_at.strftime('%Y-%m-%d %H:%M:%S') 137 | 138 | def get_tag_name_list(self, obj): 139 | issue_tags = obj.issue_tags.all() 140 | tags_content = [issue_tag.tag.content for issue_tag in issue_tags] 141 | return tags_content 142 | 143 | class Meta: 144 | model = Issue 145 | fields = ['issue_id', 'issue_title', 'content', 'user_id', 'user_name', 'user_avatar', 'chapter_id', 146 | 'chapter_name', 'subject_id', 'subject_name', 'status', 'anonymous', 'score', 147 | 'created_at', 'updated_at', 'counselor_id', 'reviewer_id', 'like_count', 'follow_count', 148 | 'tag_name_list'] 149 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from rest_framework.response import Response 3 | from rest_framework.views import APIView 4 | 5 | from service_backend.apps.notifications.models import Notification, NotificationReceiver 6 | from service_backend.apps.users.models import User 7 | from service_backend.apps.utils.views import response_json, encode_password, check_role 8 | from service_backend.apps.utils.constants import UserErrorCode, UserRole, OtherErrorCode, DEFAULT_AVATAR, \ 9 | NotificationErrorCode 10 | 11 | 12 | # Create your views here. 13 | class NotificationRead(APIView): 14 | 15 | @check_role(UserRole.ALL_USERS) 16 | def post(self, request, action_user: User = None): 17 | try: 18 | notification_receiver = NotificationReceiver.objects.get( 19 | Q(receiver_id=action_user.id) & Q(notification_id=request.data['notification_id'])) 20 | except Exception as _e: 21 | return Response(response_json( 22 | success=False, 23 | code=NotificationErrorCode.NOTIFICATION_RECEIVER_LOAD_FAILED, 24 | message="can't load notification-receiver!" 25 | )) 26 | try: 27 | notification_receiver.status = 1 28 | notification_receiver.save() 29 | except Exception as _e: 30 | return Response(response_json( 31 | success=False, 32 | code=NotificationErrorCode.NOTIFICATION_RECEIVER_SAVE_FAILED, 33 | message="can't save notification-receiver!" 34 | )) 35 | return Response(response_json( 36 | success=True, 37 | message="get notification successfully!", 38 | data={ 39 | "title": notification_receiver.notification.title, 40 | "content": notification_receiver.notification.content, 41 | "time": str(notification_receiver.notification.created_at), 42 | "category": notification_receiver.notification.category, 43 | "status": notification_receiver.status 44 | } 45 | )) 46 | 47 | 48 | # TODO: 修改为一键已读 49 | class NotificationClear(APIView): 50 | 51 | @check_role(UserRole.ALL_USERS) 52 | def post(self, request, action_user: User = None): 53 | try: 54 | notification_list = Notification.objects.filter(notification_receivers__receiver_id=action_user.id) 55 | notification_list.delete() 56 | except Exception as e: 57 | return Response(response_json( 58 | success=False, 59 | code=NotificationErrorCode.NOTIFICATION_DELETE_FAILED, 60 | message="can't delete notification!" 61 | )) 62 | return Response(response_json( 63 | success=True, 64 | message="delete notification successfully!", 65 | )) 66 | 67 | 68 | class NotificationList(APIView): 69 | 70 | @check_role(UserRole.ALL_USERS) 71 | def post(self, request, action_user: User = None): 72 | try: 73 | notification_list = NotificationReceiver.objects.filter( 74 | receiver_id=action_user.id).order_by('status', '-created_at').distinct() 75 | notification_list = notification_list.select_related('notification') 76 | except Exception as e: 77 | return Response(response_json( 78 | success=False, 79 | code=NotificationErrorCode.NOTIFICATION_RECEIVER_LOAD_FAILED, 80 | message="can't get notification-receiver!" 81 | )) 82 | return Response(response_json( 83 | success=True, 84 | message="get notification successfully!", 85 | data={ 86 | 'notification_list': [ 87 | { 88 | 'id': n.notification.id, 89 | 'title': n.notification.title, 90 | 'content': n.notification.content, 91 | 'time': str(n.notification.created_at), 92 | 'category': n.notification.category, 93 | 'status': n.status 94 | } 95 | for n in notification_list 96 | ] 97 | }, 98 | )) 99 | 100 | 101 | class NotificationBroadcast(APIView): 102 | 103 | @check_role(UserRole.ADMIN_ONLY) 104 | def post(self, request, action_user: User = None): 105 | try: 106 | notification = Notification( 107 | title=request.data['title'], 108 | content=request.data['content'], 109 | category=request.data['category'] 110 | ) 111 | notification.save() 112 | except Exception as e: 113 | return Response(response_json( 114 | success=False, 115 | code=NotificationErrorCode.NOTIFICATION_SAVE_FAILED, 116 | message="can't save notification!" 117 | )) 118 | try: 119 | NotificationReceiver.objects.bulk_create([ 120 | NotificationReceiver(notification_id=notification.id, receiver_id=receiver.id, status=0) 121 | for receiver in User.objects.all() 122 | ]) 123 | except Exception as e: 124 | return Response(response_json( 125 | success=False, 126 | code=NotificationErrorCode.NOTIFICATION_LOAD_FAILED, 127 | message="can't save notification-receiver!" 128 | )) 129 | return Response(response_json( 130 | success=True, 131 | message="broadcast notification successfully!" 132 | )) 133 | -------------------------------------------------------------------------------- /service_backend/apps/notifications/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from rest_framework.test import APITestCase 4 | 5 | from service_backend.apps.notifications.models import Notification, NotificationReceiver 6 | from service_backend.apps.users.models import User 7 | from service_backend.apps.utils.views import encode_password 8 | 9 | 10 | # Create your tests here. 11 | class NotificationAPITestCase(APITestCase): 12 | def _student_login(self): 13 | response = self.client.post('/user/user_login', {'student_id': '20373043', 'password': '123456'}) 14 | return response.data['data']['jwt'] 15 | 16 | def _admin_login(self): 17 | response = self.client.post('/user/user_login', {'student_id': '20373743', 'password': '123456'}) 18 | return response.data['data']['jwt'] 19 | 20 | def setUp(self): 21 | user = User(id=1, student_id='20373743', name='ccy', password_digest=encode_password('123456'), user_role=2, frozen=0) 22 | user.save() 23 | user = User(id=2, student_id='20373043', name='lsz', password_digest=encode_password('123456'), user_role=0, frozen=0) 24 | user.save() 25 | notification = Notification(id=1, content='1', title='11', category=1) 26 | notification.save() 27 | notification_receiver = NotificationReceiver(id=1, notification_id=1, receiver_id=2, status=0) 28 | notification_receiver.save() 29 | 30 | def test_notification_broadcast(self): 31 | jwt = self._admin_login() 32 | url = '/notification/broadcast' 33 | data = { 34 | "jwt": jwt, 35 | "title": "期末考试开卷", 36 | "content": "happy", 37 | "category": 1 38 | } 39 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 40 | self.assertEqual(response.data['code'], 0) 41 | self.assertEqual(response.data['message'], "broadcast notification successfully!") 42 | jwt = self._student_login() 43 | url = '/notification/user_receive' 44 | data = { 45 | "jwt": jwt 46 | } 47 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 48 | self.assertEqual(response.data['code'], 0) 49 | self.assertEqual(len(response.data['data']['notification_list']), 2) 50 | return 51 | 52 | def test_notification_clear(self): 53 | # create 54 | jwt = self._admin_login() 55 | url = '/notification/broadcast' 56 | data = { 57 | "jwt": jwt, 58 | "title": "期末考试开卷", 59 | "content": "happy", 60 | "category": 1 61 | } 62 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 63 | self.assertEqual(response.data['message'], "broadcast notification successfully!") 64 | data = { 65 | "jwt": jwt, 66 | "title": "期末考试闭卷", 67 | "content": "sad", 68 | "category": 1 69 | } 70 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 71 | self.assertEqual(response.data['message'], "broadcast notification successfully!") 72 | self.assertEqual(response.data['message'], "broadcast notification successfully!") 73 | data = { 74 | "jwt": jwt, 75 | "title": "期末考试取消", 76 | "content": "very happy", 77 | "category": 1 78 | } 79 | # test issue number 80 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 81 | self.assertEqual(response.data['message'], "broadcast notification successfully!") 82 | jwt = self._student_login() 83 | url = '/notification/user_receive' 84 | data = { 85 | "jwt": jwt 86 | } 87 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 88 | self.assertEqual(response.data['code'], 0) 89 | self.assertEqual(len(response.data['data']['notification_list']), 4) 90 | # clear 91 | url = '/notification/clear_all' 92 | data = { 93 | "jwt": jwt 94 | } 95 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 96 | self.assertEqual(response.data['code'], 0) 97 | # new number 98 | url = '/notification/user_receive' 99 | data = { 100 | "jwt": jwt, 101 | } 102 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 103 | self.assertEqual(response.data['code'], 0) 104 | self.assertEqual(len(response.data['data']['notification_list']), 0) 105 | return 106 | 107 | 108 | def test_notification_get(self): 109 | jwt = self._student_login() 110 | url = '/notification/user_receive' 111 | data = { 112 | "jwt": jwt 113 | } 114 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 115 | self.assertEqual(response.data['code'], 0) 116 | self.assertEqual(response.data['data']['notification_list'][0]['status'], 0) 117 | self.assertEqual(response.data['data']['notification_list'][0]['title'], '11') 118 | self.assertEqual(response.data['data']['notification_list'][0]['id'], 1) 119 | url = '/notification/get' 120 | data = { 121 | "jwt": jwt, 122 | "notification_id": 1 123 | } 124 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 125 | self.assertEqual(response.data['code'], 0) 126 | self.assertEqual(response.data['data']['status'], 1) 127 | self.assertEqual(response.data['data']['title'], '11') 128 | return 129 | -------------------------------------------------------------------------------- /service_backend/apps/issues/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-19 17:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('chapters', '0001_initial'), 13 | ('users', '__first__'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Issue', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('created_at', models.DateTimeField(auto_now_add=True)), 22 | ('updated_at', models.DateTimeField(auto_now=True)), 23 | ('title', models.CharField(max_length=255)), 24 | ('content', models.CharField(max_length=3071)), 25 | ('counsel_at', models.TimeField(null=True)), 26 | ('review_at', models.TimeField(null=True)), 27 | ('status', models.IntegerField()), 28 | ('anonymous', models.IntegerField()), 29 | ('score', models.IntegerField(null=True)), 30 | ('chapter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='chapters.chapter')), 31 | ('counselor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='counselor_issues', to='users.user')), 32 | ('reviewer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reviewer_issues', to='users.user')), 33 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_issues', to='users.user')), 34 | ], 35 | options={ 36 | 'db_table': 'issues', 37 | }, 38 | ), 39 | migrations.CreateModel( 40 | name='ReviewIssues', 41 | fields=[ 42 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('created_at', models.DateTimeField(auto_now_add=True)), 44 | ('updated_at', models.DateTimeField(auto_now=True)), 45 | ('status', models.IntegerField()), 46 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='review_issues', to='issues.issue')), 47 | ('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviewer_review_issues', to='users.user')), 48 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_review_issues', to='users.user')), 49 | ], 50 | options={ 51 | 'db_table': 'review_issues', 52 | }, 53 | ), 54 | migrations.CreateModel( 55 | name='LikeIssues', 56 | fields=[ 57 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 58 | ('created_at', models.DateTimeField(auto_now_add=True)), 59 | ('updated_at', models.DateTimeField(auto_now=True)), 60 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='like_issues', to='issues.issue')), 61 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='like_issues', to='users.user')), 62 | ], 63 | options={ 64 | 'db_table': 'like_issues', 65 | }, 66 | ), 67 | migrations.CreateModel( 68 | name='FollowIssues', 69 | fields=[ 70 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('created_at', models.DateTimeField(auto_now_add=True)), 72 | ('updated_at', models.DateTimeField(auto_now=True)), 73 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follow_issues', to='issues.issue')), 74 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follow_issues', to='users.user')), 75 | ], 76 | options={ 77 | 'db_table': 'follow_issues', 78 | }, 79 | ), 80 | migrations.CreateModel( 81 | name='Comment', 82 | fields=[ 83 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 84 | ('created_at', models.DateTimeField(auto_now_add=True)), 85 | ('updated_at', models.DateTimeField(auto_now=True)), 86 | ('content', models.CharField(max_length=3071)), 87 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='issues.issue')), 88 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='users.user')), 89 | ], 90 | options={ 91 | 'db_table': 'comments', 92 | }, 93 | ), 94 | migrations.CreateModel( 95 | name='AdoptIssues', 96 | fields=[ 97 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 98 | ('created_at', models.DateTimeField(auto_now_add=True)), 99 | ('updated_at', models.DateTimeField(auto_now=True)), 100 | ('status', models.IntegerField()), 101 | ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adopt_issues', to='issues.issue')), 102 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adopt_issues', to='users.user')), 103 | ], 104 | options={ 105 | 'db_table': 'adopt_issues', 106 | }, 107 | ), 108 | ] 109 | -------------------------------------------------------------------------------- /service_backend/apps/utils/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count 2 | 3 | from service_backend.apps.utils.constants import GlobalCode, UserErrorCode 4 | from service_backend.apps.users.models import User, BlackList 5 | from service_backend.apps.issues.models import Issue, ReviewIssues, AdoptIssues, LikeIssues, FollowIssues 6 | from service_backend.apps.years.models import Year 7 | from service_backend.apps.chapters.models import Chapter 8 | from service_backend.apps.subjects.models import Subject, UserSubject 9 | from service_backend.settings import ENV 10 | from rest_framework.response import Response 11 | from datetime import datetime 12 | from jwt import encode, decode 13 | from hashlib import sha256 14 | from functools import wraps 15 | 16 | 17 | def response_json(success, code=None, message=None, data=None): 18 | success = not not success 19 | code = (GlobalCode.SUCCESS if success else GlobalCode.SYSTEM_FAILED) if code is None else code 20 | message = ("Success!" if success else "Failed!") if message is None else message 21 | 22 | return { 23 | 'success': success, 24 | 'code': code, 25 | 'message': message, 26 | 'data': data 27 | } 28 | 29 | 30 | def decode_jwt(token: str) -> (int, dict): 31 | user_id, message, code, response = None, '', None, None 32 | try: 33 | jwt_dict = decode(jwt=token, algorithms='HS256', key=ENV['JWT_KEY']) 34 | start_time = datetime.fromisoformat(jwt_dict['time']) 35 | seconds = (datetime.now() - start_time).seconds 36 | if seconds < ENV['JWT_LIFETIME'] and not BlackList.objects.filter(token=token).exists(): 37 | user_id = jwt_dict['user_id'] 38 | else: 39 | message, code = 'expired jwt token!', UserErrorCode.EXPIRED_JWT 40 | except Exception as e: 41 | message, code = 'invalid jwt token!', UserErrorCode.INVALID_JWT 42 | if not user_id: 43 | response = response_json(success=False, code=code, message=message) 44 | return user_id, response 45 | 46 | 47 | def generate_jwt(user_id: int) -> str: 48 | cur_time = str(datetime.now()) 49 | token = encode(payload={'user_id': user_id, 'time': cur_time}, algorithm='HS256', 50 | key=ENV['JWT_KEY'], headers={'typ': 'JWT', 'alg': 'HS256'}) 51 | return token 52 | 53 | 54 | def check_jwt(f): 55 | @wraps(f) 56 | def wrapper(*args, **kwargs): 57 | token = args[1].data['jwt'] # args = (, ) 58 | user_id, response = decode_jwt(token) 59 | if not user_id: 60 | return Response(response) 61 | return f(*args, **kwargs, user_id=user_id) 62 | 63 | return wrapper 64 | 65 | 66 | def check_role(role_list: list): 67 | def decorated(f): 68 | @wraps(f) 69 | def wrapper(*args, **kwargs): 70 | # check jwt, args = (, ) 71 | if args[1].data.__contains__('jwt'): 72 | token = args[1].data['jwt'] 73 | else: 74 | token = args[1].META['HTTP_TOKEN'] 75 | user_id, response = decode_jwt(token) 76 | # print(user_id) 77 | if not user_id: 78 | return Response(response) 79 | # check authority 80 | user = User.objects.get(id=user_id) 81 | # print(user.id, user.user_role) 82 | if user.id and user.frozen: # have such user and frozen 83 | return Response(response_json( 84 | success=False, 85 | code=UserErrorCode.USER_FROZEN, 86 | message='your account has been frozen, please contact administrator!' 87 | )) 88 | # print(user.user_role, role_list) 89 | if not user.id or not (user.user_role in role_list): 90 | return Response(response_json( 91 | success=False, 92 | code=UserErrorCode.PERMISSION_DENIED, 93 | message='permission denied!' 94 | )) 95 | # print(args[1].data) 96 | return f(*args, **kwargs, action_user=user) 97 | 98 | return wrapper 99 | 100 | return decorated 101 | 102 | 103 | def encode_password(message: str, salt=ENV['PASSWORD_SALT']) -> str: 104 | h = sha256() 105 | message += salt 106 | h.update(message.encode()) 107 | for i in range(1000): 108 | h.update(h.hexdigest().encode()) 109 | return h.hexdigest() 110 | 111 | 112 | def init_database(): 113 | User.objects.all().delete() 114 | User.objects.bulk_create([ 115 | User(student_id='20373743', name='ccy', password_digest=encode_password('123456'), user_role=2, frozen=0), 116 | User(student_id='20373043', name='lsz', password_digest=encode_password('123456'), user_role=0, frozen=0), 117 | User(student_id='20373044', name='xyy', password_digest=encode_password('123456'), user_role=1, frozen=0), 118 | User(student_id='20373045', name='xxx', password_digest=encode_password('123456'), user_role=1, frozen=0), 119 | ]) 120 | Year.objects.all().delete() 121 | Year.objects.bulk_create([Year(content='2023年')]) 122 | Subject.objects.all().delete() 123 | Subject.objects.bulk_create([ 124 | Subject(name='数学分析2', content='...', year_id=1), 125 | Subject(name='大学物理', content='...', year_id=1), 126 | ]) 127 | Chapter.objects.all().delete() 128 | Chapter.objects.bulk_create([ 129 | Chapter(subject_id=1, name='多元函数求导', content='hh'), 130 | Chapter(subject_id=2, name='角动量', content='hhh'), 131 | ]) 132 | UserSubject.objects.all().delete() 133 | UserSubject.objects.bulk_create([ 134 | UserSubject(user_id=3, subject_id=1), 135 | UserSubject(user_id=3, subject_id=2), 136 | ]) 137 | Issue.objects.all().delete() 138 | Issue.objects.bulk_create([ 139 | Issue(title='1', content='123', user_id=1, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, anonymous=0, 140 | score=0), 141 | Issue(title='2', content='123', user_id=1, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, anonymous=0, 142 | score=0), 143 | Issue(title='3', content='123', user_id=1, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, anonymous=0, 144 | score=0), 145 | Issue(title='4', content='123', user_id=1, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, anonymous=0, 146 | score=0), 147 | Issue(title='5', content='123', user_id=1, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, anonymous=0, 148 | score=0) 149 | ]) 150 | ReviewIssues.objects.all().delete() 151 | ReviewIssues.objects.bulk_create([ 152 | ReviewIssues(user_id=3, reviewer_id=1, issue_id=1, status=0), 153 | ReviewIssues(user_id=3, reviewer_id=1, issue_id=3, status=0), 154 | ReviewIssues(user_id=3, reviewer_id=1, issue_id=5, status=0), 155 | ReviewIssues(user_id=4, reviewer_id=1, issue_id=4, status=0), 156 | ]) 157 | 158 | # init_database() 159 | 160 | # print(generate_jwt(10001)) 161 | -------------------------------------------------------------------------------- /service_backend/apps/issues/views_issue_status.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | 6 | from service_backend.apps.issues.views_issue import find_issue, status_trans_permit 7 | from service_backend.apps.issues.models import AdoptIssues, ReviewIssues 8 | from service_backend.apps.utils.constants import UserRole, IssueStatus, IssueErrorCode, IssueReviewerErrorCode, \ 9 | OtherErrorCode 10 | from service_backend.apps.utils.views import response_json, check_role 11 | 12 | 13 | class IssueAdopt(APIView): 14 | @check_role([UserRole.TUTOR, UserRole.ADMIN, ]) 15 | @find_issue() 16 | def post(self, request, issue, action_user): 17 | if status_trans_permit(issue, action_user)[0] != 1: 18 | return Response(response_json( 19 | success=False, 20 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 21 | message="you have no access to this issue!" 22 | ), status=404) 23 | 24 | issue.counselor = action_user 25 | issue.counsel_at = datetime.datetime.now() 26 | issue.status = IssueStatus.ADOPTING 27 | try: 28 | issue.save() 29 | except Exception: 30 | return Response(response_json( 31 | success=False, 32 | code=IssueErrorCode.ISSUE_SAVED_FAILED, 33 | message="can't adopt issue!" 34 | ), status=404) 35 | adopt_issue = AdoptIssues(issue=issue, user=action_user, status=0) 36 | try: 37 | adopt_issue.save() 38 | except Exception: 39 | return Response(response_json( 40 | success=False, 41 | code=IssueReviewerErrorCode.REVIEWER_ISSUE_SAVED_FAILED, 42 | message="can't review issue!" 43 | ), status=404) 44 | 45 | return Response(response_json( 46 | success=True, 47 | message="adopt issue success!" 48 | )) 49 | 50 | 51 | class IssueCancel(APIView): 52 | @check_role([UserRole.STUDENT, UserRole.ADMIN, ]) 53 | @find_issue() 54 | def post(self, request, issue, action_user): 55 | if status_trans_permit(issue, action_user)[1] != 1: 56 | return Response(response_json( 57 | success=False, 58 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 59 | message="you have no access to this issue!" 60 | ), status=404) 61 | 62 | issue.status = IssueStatus.INVALID_ISSUE 63 | try: 64 | issue.save() 65 | except Exception: 66 | return Response(response_json( 67 | success=False, 68 | code=IssueErrorCode.ISSUE_SAVED_FAILED, 69 | message="can't cancel issue!" 70 | ), status=404) 71 | return Response(response_json( 72 | success=True, 73 | message="cancel issue success!" 74 | )) 75 | 76 | 77 | class IssueReject(APIView): 78 | @check_role([UserRole.STUDENT, UserRole.ADMIN, ]) 79 | @find_issue() 80 | def post(self, request, issue, action_user): 81 | if status_trans_permit(issue, action_user)[2] != 1: 82 | return Response(response_json( 83 | success=False, 84 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 85 | message="you have no access to this issue!" 86 | ), status=404) 87 | 88 | issue.counselor = None 89 | issue.counsel_at = None 90 | issue.status = IssueStatus.NOT_ADOPT 91 | try: 92 | issue.save() 93 | except Exception: 94 | return Response(response_json( 95 | success=False, 96 | code=IssueErrorCode.ISSUE_SAVED_FAILED, 97 | message="can't reject issue!" 98 | ), status=404) 99 | return Response(response_json( 100 | success=True, 101 | message="reject issue success!" 102 | )) 103 | 104 | 105 | class IssueAgree(APIView): 106 | @check_role([UserRole.STUDENT, UserRole.ADMIN, ]) 107 | @find_issue() 108 | def post(self, request, issue, action_user): 109 | if status_trans_permit(issue, action_user)[3] != 1: 110 | return Response(response_json( 111 | success=False, 112 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 113 | message="you have no access to this issue!" 114 | ), status=404) 115 | 116 | issue.status = IssueStatus.NOT_REVIEW 117 | try: 118 | issue.save() 119 | except Exception: 120 | return Response(response_json( 121 | success=False, 122 | code=IssueErrorCode.ISSUE_SAVED_FAILED, 123 | message="can't review issue!" 124 | ), status=404) 125 | return Response(response_json( 126 | success=True, 127 | message="agree review success!" 128 | )) 129 | 130 | 131 | class IssueReview(APIView): 132 | @check_role([UserRole.TUTOR, UserRole.ADMIN, ]) 133 | @find_issue() 134 | def post(self, request, issue, action_user): 135 | if status_trans_permit(issue, action_user)[4] != 1: 136 | return Response(response_json( 137 | success=False, 138 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 139 | message="you have no access to this issue!" 140 | ), status=404) 141 | 142 | issue.reviewer = action_user 143 | issue.status = IssueStatus.REVIEWING 144 | try: 145 | issue.save() 146 | except Exception: 147 | return Response(response_json( 148 | success=False, 149 | code=IssueErrorCode.ISSUE_SAVED_FAILED, 150 | message="can't review issue!" 151 | ), status=404) 152 | 153 | reviewer_issue = ReviewIssues(issue=issue, user=action_user, reviewed=issue.counselor) 154 | try: 155 | reviewer_issue.save() 156 | except Exception: 157 | return Response(response_json( 158 | success=False, 159 | code=IssueReviewerErrorCode.REVIEWER_ISSUE_SAVED_FAILED, 160 | message="can't review issue!" 161 | ), status=404) 162 | 163 | return Response(response_json( 164 | success=True, 165 | message="adopt review success!" 166 | )) 167 | 168 | 169 | class IssueReadopt(APIView): 170 | @check_role([UserRole.TUTOR, UserRole.ADMIN, ]) 171 | @find_issue() 172 | def post(self, request, issue, action_user): 173 | if status_trans_permit(issue, action_user)[5] != 1: 174 | return Response(response_json( 175 | success=False, 176 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 177 | message="you have no access to this issue!" 178 | ), status=404) 179 | 180 | issue.status = IssueStatus.ADOPTING 181 | issue.counselor = action_user 182 | issue.counsel_at = datetime.datetime.now() 183 | 184 | adopt_issue = AdoptIssues(issue=issue, user=action_user, status=0) 185 | try: 186 | adopt_issue.save() 187 | except Exception: 188 | return Response(response_json( 189 | success=False, 190 | code=IssueReviewerErrorCode.REVIEWER_ISSUE_SAVED_FAILED, 191 | message="can't readopt issue!" 192 | ), status=404) 193 | 194 | try: 195 | reviewer_issue = ReviewIssues.objects.filter(issue=issue, user=action_user) 196 | reviewer_issue.first().status = 0 197 | reviewer_issue.first().save() 198 | issue.save() 199 | except Exception: 200 | return Response(response_json( 201 | success=False, 202 | code=IssueErrorCode.ISSUE_SAVED_FAILED, 203 | message="can't readopt issue!" 204 | ), status=404) 205 | return Response(response_json( 206 | success=True, 207 | message="readopt issue success!" 208 | )) 209 | 210 | 211 | class IssueClassify(APIView): 212 | @check_role([UserRole.TUTOR, UserRole.ADMIN, ]) 213 | @find_issue() 214 | def post(self, request, issue, action_user): 215 | if status_trans_permit(issue, action_user)[6] != 1: 216 | return Response(response_json( 217 | success=False, 218 | code=IssueErrorCode.ISSUE_ACTION_REJECT, 219 | message="you have no access to this issue!" 220 | ), status=404) 221 | 222 | if request.data['is_valid'] == 1: 223 | issue.status = IssueStatus.VALID_ISSUE 224 | elif request.data['is_valid'] == 0: 225 | issue.status = IssueStatus.INVALID_ISSUE 226 | else: 227 | return Response(response_json( 228 | success=False, 229 | code=OtherErrorCode.UNEXPECTED_JSON_FORMAT, 230 | message="is_valid is not valid!" 231 | ), status=404) 232 | 233 | try: 234 | reviewer_issue = ReviewIssues.objects.filter(issue=issue, user=action_user) 235 | reviewer_issue.first().status = 1 236 | reviewer_issue.first().save() 237 | issue.save() 238 | except Exception: 239 | return Response(response_json( 240 | success=False, 241 | code=IssueErrorCode.ISSUE_SAVED_FAILED, 242 | message="can't classify issue!" 243 | ), status=404) 244 | return Response(response_json( 245 | success=True, 246 | message="classify issue success!" 247 | )) 248 | -------------------------------------------------------------------------------- /service_backend/apps/admins/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | from rest_framework.test import APITestCase 4 | from service_backend.apps.users.models import User 5 | from service_backend.apps.issues.models import Issue, ReviewIssues, AdoptIssues, LikeIssues, FollowIssues, IssueApiCall 6 | from service_backend.apps.years.models import Year 7 | from service_backend.apps.chapters.models import Chapter 8 | from service_backend.apps.subjects.models import Subject, UserSubject 9 | from service_backend.apps.utils.views import encode_password, decode_jwt 10 | 11 | 12 | # Create your tests here. 13 | class AdminAPITestCase(APITestCase): 14 | 15 | def setUp(self): 16 | User.objects.bulk_create([ 17 | User(id=1, student_id='20373743', name='ccy', password_digest=encode_password('123456'), user_role=2, 18 | frozen=0), 19 | User(id=2, student_id='20373043', name='lsz', password_digest=encode_password('123456'), user_role=0, 20 | frozen=0), 21 | User(id=3, student_id='20373044', name='xyy', password_digest=encode_password('123456'), user_role=1, 22 | frozen=0), 23 | User(id=4, student_id='20373045', name='xxx', password_digest=encode_password('123456'), user_role=1, 24 | frozen=0), 25 | User(id=5, student_id='20373046', name='yyy', password_digest=encode_password('123456'), user_role=1, 26 | frozen=0), 27 | ]) 28 | Year.objects.bulk_create([Year(id=1, content='2023年')]) 29 | Subject.objects.bulk_create([ 30 | Subject(id=1, name='数学分析2', content='...', year_id=1), 31 | Subject(id=2, name='大学物理', content='...', year_id=1), 32 | ]) 33 | Chapter.objects.bulk_create([ 34 | Chapter(id=1, subject_id=1, name='多元函数求导', content='hh'), 35 | Chapter(id=2, subject_id=2, name='角动量', content='hhh'), 36 | ]) 37 | UserSubject.objects.bulk_create([ 38 | UserSubject(id=1, user_id=3, subject_id=1), 39 | UserSubject(id=2, user_id=3, subject_id=2), 40 | ]) 41 | Issue.objects.bulk_create([ 42 | Issue(id=1, title='1', content='123', user_id=1, chapter_id=1, counselor_id=4, reviewer_id=3, status=0, 43 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 22, 23, 59, 59), 44 | review_at=datetime(2023, 5, 23, 0, 0, 0)), 45 | Issue(id=2, title='2', content='123', user_id=1, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, 46 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 23, 0, 0, 0), 47 | review_at=datetime(2023, 5, 24, 0, 0, 1)), 48 | Issue(id=3, title='3', content='123', user_id=1, chapter_id=1, counselor_id=4, reviewer_id=3, status=0, 49 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 25, 0, 0, 0), 50 | review_at=datetime(2023, 5, 27, 23, 59, 59)), 51 | Issue(id=4, title='4', content='123', user_id=1, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, 52 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 25, 23, 59, 59), 53 | review_at=datetime(2023, 5, 28, 0, 0, 0)), 54 | Issue(id=5, title='5', content='123', user_id=1, chapter_id=1, counselor_id=4, reviewer_id=3, status=0, 55 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 28, 0, 0, 0), 56 | review_at=datetime(2023, 5, 28, 0, 0, 1)), 57 | ]) 58 | ReviewIssues.objects.bulk_create([ 59 | ReviewIssues(id=1, user_id=3, reviewed_id=1, issue_id=1, status=0), 60 | ReviewIssues(id=2, user_id=3, reviewed_id=1, issue_id=3, status=0), 61 | ReviewIssues(id=3, user_id=3, reviewed_id=1, issue_id=5, status=0), 62 | ReviewIssues(id=4, user_id=4, reviewed_id=1, issue_id=2, status=0), 63 | ReviewIssues(id=5, user_id=4, reviewed_id=1, issue_id=4, status=0), 64 | ]) 65 | AdoptIssues.objects.bulk_create([ 66 | AdoptIssues(id=1, user_id=4, issue_id=1, status=0), 67 | AdoptIssues(id=2, user_id=4, issue_id=3, status=0), 68 | AdoptIssues(id=3, user_id=4, issue_id=5, status=0), 69 | AdoptIssues(id=4, user_id=3, issue_id=2, status=0), 70 | AdoptIssues(id=5, user_id=3, issue_id=4, status=0), 71 | ]) 72 | FollowIssues.objects.bulk_create([ 73 | FollowIssues(id=1, user_id=2, issue_id=1), 74 | FollowIssues(id=2, user_id=2, issue_id=3), 75 | FollowIssues(id=3, user_id=1, issue_id=5), 76 | FollowIssues(id=4, user_id=1, issue_id=4), 77 | ]) 78 | IssueApiCall.objects.bulk_create([ 79 | IssueApiCall(id=1, created_at=datetime(2023, 5, 22, 23, 59, 59), user_id=1, issue_id=2), 80 | IssueApiCall(id=2, created_at=datetime(2023, 5, 23, 0, 0, 0), user_id=2, issue_id=2), 81 | IssueApiCall(id=3, created_at=datetime(2023, 5, 23, 23, 59, 59), user_id=3, issue_id=3), 82 | IssueApiCall(id=4, created_at=datetime(2023, 5, 25, 0, 0, 0), user_id=3, issue_id=4), 83 | IssueApiCall(id=5, created_at=datetime(2023, 5, 26, 0, 0, 0), user_id=5, issue_id=2), 84 | IssueApiCall(id=6, created_at=datetime(2023, 5, 26, 0, 0, 0), user_id=2, issue_id=4), 85 | IssueApiCall(id=7, created_at=datetime(2023, 5, 27, 23, 59, 59), user_id=4, issue_id=2), 86 | IssueApiCall(id=8, created_at=datetime(2023, 5, 28, 0, 0, 0), user_id=2, issue_id=3), 87 | ]) 88 | issue_call_api = IssueApiCall.objects.get(id=1) 89 | issue_call_api.created_at = datetime(2023, 5, 22, 23, 59, 59) 90 | issue_call_api.save() 91 | issue_call_api = IssueApiCall.objects.get(id=2) 92 | issue_call_api.created_at = datetime(2023, 5, 23, 0, 0, 0) 93 | issue_call_api.save() 94 | issue_call_api = IssueApiCall.objects.get(id=3) 95 | issue_call_api.created_at = datetime(2023, 5, 23, 23, 59, 59) 96 | issue_call_api.save() 97 | issue_call_api = IssueApiCall.objects.get(id=4) 98 | issue_call_api.created_at = datetime(2023, 5, 25, 0, 0, 0) 99 | issue_call_api.save() 100 | issue_call_api = IssueApiCall.objects.get(id=5) 101 | issue_call_api.created_at = datetime(2023, 5, 26, 0, 0, 0) 102 | issue_call_api.save() 103 | issue_call_api = IssueApiCall.objects.get(id=6) 104 | issue_call_api.created_at = datetime(2023, 5, 26, 0, 0, 0) 105 | issue_call_api.save() 106 | issue_call_api = IssueApiCall.objects.get(id=7) 107 | issue_call_api.created_at = datetime(2023, 5, 27, 23, 59, 59) 108 | issue_call_api.save() 109 | issue_call_api = IssueApiCall.objects.get(id=8) 110 | issue_call_api.created_at = datetime(2023, 5, 28, 0, 0, 0) 111 | issue_call_api.save() 112 | 113 | def _admin_login(self): 114 | response = self.client.post('/user/user_login', {'student_id': '20373743', 'password': '123456'}) 115 | return response.data['data']['jwt'] 116 | 117 | def test_create_user(self): 118 | jwt_token = self._admin_login() 119 | url = '/admins/create_user' 120 | data = { 121 | 'jwt': jwt_token, 122 | 'student_id': 20373742, 123 | 'name': 'son', 124 | 'password': '123456789', 125 | 'role': 2 126 | } 127 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 128 | self.assertEqual(response.status_code, 200) 129 | self.assertEqual(response.data['message'], 'create user successfully!') 130 | self.assertEqual(response.data['code'], 0) 131 | self.client.post('/user/user_login', {'student_id': '20373742', 'password': '123456789'}) 132 | 133 | def test_create_user_batch(self): 134 | jwt_token = self._admin_login() 135 | url = '/admins/create_user_batch' 136 | data = { 137 | 'jwt': jwt_token, 138 | 'student_id_list': [20373744, 20373745], 139 | 'name_list': ['son', 'grandson'], 140 | 'password_list': ['123456789', '66666666'], 141 | 'role_list': [2, 1] 142 | } 143 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 144 | self.assertEqual(response.status_code, 200) 145 | self.assertEqual(response.data['message'], "batch create user successfully!") 146 | self.assertEqual(response.data['code'], 0) 147 | self.client.post('/user/user_login', {'student_id': '20373744', 'password': '123456789'}) 148 | self.client.post('/user/user_login', {'student_id': '20373745', 'password': '66666666'}) 149 | 150 | def test_users(self): 151 | jwt_token = self._admin_login() 152 | url = '/admins/users' 153 | data = { 154 | 'jwt': jwt_token 155 | } 156 | response = self.client.post(url, data) 157 | self.assertEqual(response.status_code, 200) 158 | self.assertEqual(response.data['message'], 'get user list successfully!') 159 | self.assertEqual(response.data['code'], 0) 160 | self.assertEqual({user['user_id'] for user in response.data['data']['user_list']}, {1, 2, 3, 4, 5}) 161 | self.assertEqual({user['name'] for user in response.data['data']['user_list']}, 162 | {'ccy', 'lsz', 'xxx', 'xyy', 'yyy'}) 163 | 164 | def test_update_user_tole(self): 165 | jwt_token = self._admin_login() 166 | url = '/admins/update_privilege' 167 | data = { 168 | 'user_id': 3, 169 | 'user_role': 0, 170 | 'jwt': jwt_token 171 | } 172 | response = self.client.post(url, data) 173 | self.assertEqual(response.status_code, 200) 174 | self.assertEqual(response.data['message'], "update user role successfully!") 175 | self.assertEqual(response.data['code'], 0) 176 | 177 | def test_freeze_user(self): 178 | jwt_token = self._admin_login() 179 | url = '/admins/freeze_user' 180 | data = { 181 | 'user_id': 3, 182 | 'frozen': 1, 183 | 'jwt': jwt_token 184 | } 185 | response = self.client.post(url, data) 186 | self.assertEqual(response.status_code, 200) 187 | self.assertEqual(response.data['message'], "update frozen status successfully!") 188 | self.assertEqual(response.data['code'], 0) 189 | 190 | def test_issue_delete(self): 191 | jwt_token = self._admin_login() 192 | url = '/admins/issue/delete' 193 | data = { 194 | 'issue_id': 3, 195 | 'jwt': jwt_token 196 | } 197 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 198 | self.assertEqual(response.status_code, 200) 199 | self.assertEqual(response.data['message'], "delete issue successfully!") 200 | self.assertEqual(response.data['code'], 0) 201 | 202 | def test_statistics(self): 203 | jwt = self._admin_login() 204 | url = '/admins/statistics' 205 | begin_date, end_date = "2023-05-23", "2023-05-27" 206 | expected_res = { 207 | (0, 0): [1, 0, 2, 0, 0], 208 | (0, 1): [2, 1], # [0, 0, 2, 1, 0], 209 | (1, 0): [1, 1, 0, 0, 1], 210 | (1, 1): [2, 1], # [0, 0, 2, 1, 0], 211 | (2, 0): [2, 0, 1, 2, 1], 212 | (2, 1): [3, 1, 2], 213 | } 214 | for k, v in expected_res.items(): 215 | data = { 216 | "jwt": jwt, 217 | "type": k[1], 218 | "indicator": k[0], 219 | "begin_date": begin_date, 220 | "end_date": end_date 221 | } 222 | json.dumps(data) 223 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 224 | self.assertEqual(response.data['code'], 0) 225 | self.assertEqual(response.data['data']['list'], v) 226 | 227 | def test_student_bonus(self): 228 | Issue.objects.bulk_create([ 229 | Issue(id=6, title='6', content='666', user_id=2, chapter_id=1, counselor_id=4, reviewer_id=3, status=0, 230 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 22, 23, 59, 59), 231 | review_at=datetime(2023, 5, 23, 0, 0, 0)), 232 | Issue(id=7, title='7', content='777', user_id=2, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, 233 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 23, 0, 0, 0), 234 | review_at=datetime(2023, 5, 24, 0, 0, 1)), 235 | ]) 236 | # haha 237 | jwt = self._admin_login() 238 | url = '/admins/student_bonus' 239 | begin_date, end_date = "2023-05-23", "2040-05-27" 240 | data = { 241 | 'issue_id': 3, 242 | 'bonus_per_issue': 1.4, 243 | 'begin_date': begin_date, 244 | 'end_date': end_date, 245 | 'max_bonus': 50, 246 | 'min_bonus': 0, 247 | 'jwt': jwt 248 | } 249 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 250 | self.assertEqual(response.data['code'], 0) 251 | self.assertEqual(len(response.data['data']['bonus_list']), 2) 252 | self.assertEqual({bonus['bonus'] for bonus in response.data['data']['bonus_list']}, {3.0, 7.0}) 253 | 254 | def test_tutor_bonus(self): 255 | Issue.objects.bulk_create([ 256 | Issue(id=6, title='6', content='666', user_id=2, chapter_id=1, counselor_id=4, reviewer_id=3, status=0, 257 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 22, 23, 59, 59), 258 | review_at=datetime(2023, 5, 23, 0, 0, 0)), 259 | Issue(id=7, title='7', content='777', user_id=2, chapter_id=1, counselor_id=3, reviewer_id=4, status=0, 260 | anonymous=0, score=0, counsel_at=datetime(2023, 5, 23, 0, 0, 0), 261 | review_at=datetime(2023, 5, 24, 0, 0, 1)), 262 | ]) 263 | # haha 264 | jwt = self._admin_login() 265 | url = '/admins/tutor_bonus' 266 | begin_date, end_date = "2023-05-10", "2040-05-27" 267 | data = { 268 | 'issue_id': 3, 269 | 'bonus_per_counsel': 1.5, 270 | 'bonus_per_review': 2.0, 271 | 'begin_date': begin_date, 272 | 'end_date': end_date, 273 | 'max_bonus': 50, 274 | 'min_bonus': 0, 275 | 'jwt': jwt 276 | } 277 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 278 | self.assertEqual(response.data['code'], 0) 279 | self.assertEqual(len(response.data['data']['bonus_list']), 2) 280 | self.assertEqual({bonus['bonus'] for bonus in response.data['data']['bonus_list']}, {12.5, 12.0}) 281 | -------------------------------------------------------------------------------- /service_backend/apps/issues/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from rest_framework.test import APITestCase 4 | 5 | from service_backend.apps.chapters.models import Chapter 6 | from service_backend.apps.issues.models import Issue, Comment 7 | from service_backend.apps.subjects.models import Subject, UserSubject 8 | from service_backend.apps.tags.models import Tag, IssueTag 9 | from service_backend.apps.users.models import User 10 | from service_backend.apps.utils.constants import IssueStatus 11 | from service_backend.apps.utils.views import encode_password 12 | from service_backend.apps.years.models import Year 13 | 14 | 15 | # Create your tests here. 16 | class IssueAPITestCase(APITestCase): 17 | def _student_login(self): 18 | response = self.client.post('/user/user_login', {'student_id': '20373043', 'password': '123456'}) 19 | return response.data['data']['jwt'] 20 | 21 | def _student_login_2(self): 22 | response = self.client.post('/user/user_login', {'student_id': '20373228', 'password': '123456'}) 23 | return response.data['data']['jwt'] 24 | 25 | def _tutor_login_1(self): 26 | response = self.client.post('/user/user_login', {'student_id': '20373044', 'password': '123456'}) 27 | return response.data['data']['jwt'] 28 | 29 | def _tutor_login_2(self): 30 | response = self.client.post('/user/user_login', {'student_id': '20373290', 'password': '123456'}) 31 | return response.data['data']['jwt'] 32 | 33 | def _admin_login(self): 34 | response = self.client.post('/user/user_login', {'student_id': '20373743', 'password': '123456'}) 35 | return response.data['data']['jwt'] 36 | 37 | def setUp(self): 38 | user = User(student_id='20373743', name='ccy', password_digest=encode_password('123456'), user_role=2, frozen=0) 39 | user.save() 40 | user = User(student_id='20373044', name='xyy', password_digest=encode_password('123456'), user_role=1, frozen=0) 41 | user.save() 42 | user = User(student_id='20373290', name='aaa', password_digest=encode_password('123456'), user_role=1, frozen=0) 43 | user.save() 44 | user = User(student_id='20373228', name='bbb', password_digest=encode_password('123456'), user_role=0, frozen=0) 45 | user.save() 46 | user = User(student_id='20373043', name='lsz', password_digest=encode_password('123456'), user_role=0, frozen=0) 47 | user.save() 48 | 49 | year = Year(content="year") 50 | year.save() 51 | self.year = year 52 | 53 | subject = Subject(name='subject', content='content', year=year) 54 | subject.save() 55 | self.subject = subject 56 | 57 | user_subject = UserSubject(user=User.objects.get(student_id='20373044'), subject=subject) 58 | user_subject.save() 59 | user_subject = UserSubject(user=User.objects.get(student_id='20373290'), subject=subject) 60 | user_subject.save() 61 | 62 | chapter = Chapter(name='chapter_1', content='content_1', subject=subject) 63 | chapter.save() 64 | self.chapter = chapter 65 | chapter = Chapter(name='chapter_2', content='content_2', subject=subject) 66 | chapter.save() 67 | 68 | issue = Issue(title='关于数学分析中无穷级数的相关问题', content="内容测试", user=user, chapter=self.chapter, 69 | status=IssueStatus.NOT_ADOPT, anonymous=0) 70 | issue.save() 71 | self.issue = issue 72 | issue = Issue(title='问题测试2', content="内容测试2", user=user, chapter=self.chapter, 73 | status=IssueStatus.NOT_ADOPT, anonymous=0) 74 | issue.save() 75 | self.issue_2 = issue 76 | 77 | tag = Tag(content="tag_1") 78 | tag.save() 79 | self.tag_1 = tag 80 | tag = Tag(content="tag_2") 81 | tag.save() 82 | self.tag_2 = tag 83 | 84 | issue_tag = IssueTag(issue=self.issue, tag=self.tag_1) 85 | issue_tag.save() 86 | issue_tag = IssueTag(issue=self.issue_2, tag=self.tag_2) 87 | issue_tag.save() 88 | 89 | comment = Comment(content="comment_1", issue=issue, user=user) 90 | comment.save() 91 | self.comment = comment 92 | comment = Comment(content="comment_2", issue=issue, user=user) 93 | comment.save() 94 | return 95 | 96 | def test_issue_get(self): 97 | jwt = self._student_login() 98 | url = '/issue/get' 99 | data = { 100 | "jwt": jwt, 101 | "issue_id": self.issue.id 102 | } 103 | response = self.client.post(url, data) 104 | self.assertEqual(response.data['code'], 0) 105 | return 106 | 107 | def test_issue_cancel(self): 108 | jwt = self._student_login() 109 | url = '/issue/cancel' 110 | data = { 111 | "jwt": jwt, 112 | "issue_id": self.issue.id 113 | } 114 | response = self.client.post(url, data) 115 | self.assertEqual(response.data['code'], 0) 116 | return 117 | 118 | def test_issue_changes(self): 119 | jwt_tutor_1 = self._tutor_login_1() 120 | jwt_tutor_2 = self._tutor_login_2() 121 | jwt_student = self._student_login() 122 | adopt_url = '/issue/adopt' 123 | reject_url = '/issue/reject' 124 | agree_url = '/issue/agree' 125 | review_url = '/issue/review' 126 | readopt_url = '/issue/readopt' 127 | classify_url = '/issue/classify' 128 | student_data = { 129 | "jwt": jwt_student, 130 | "issue_id": self.issue.id 131 | } 132 | tutor_data_1 = { 133 | "jwt": jwt_tutor_1, 134 | "issue_id": self.issue.id 135 | } 136 | tutor_data_2 = { 137 | "jwt": jwt_tutor_2, 138 | "issue_id": self.issue.id 139 | } 140 | response = self.client.post(adopt_url, tutor_data_1) 141 | self.assertEqual(response.data['code'], 0) 142 | response = self.client.post(reject_url, student_data) 143 | self.assertEqual(response.data['code'], 0) 144 | response = self.client.post(adopt_url, tutor_data_2) 145 | self.assertEqual(response.data['code'], 0) 146 | response = self.client.post(agree_url, student_data) 147 | self.assertEqual(response.data['code'], 0) 148 | response = self.client.post(review_url, tutor_data_1) 149 | self.assertEqual(response.data['code'], 0) 150 | response = self.client.post(readopt_url, tutor_data_1) 151 | self.assertEqual(response.data['code'], 0) 152 | response = self.client.post(agree_url, student_data) 153 | self.assertEqual(response.data['code'], 0) 154 | response = self.client.post(review_url, tutor_data_2) 155 | self.assertEqual(response.data['code'], 0) 156 | tutor_data_2["is_valid"] = 0 157 | response = self.client.post(classify_url, json.dumps(tutor_data_2), content_type='application/json') 158 | self.assertEqual(response.data['code'], 0) 159 | return 160 | 161 | def test_issue_commit(self): 162 | jwt = self._student_login() 163 | url = '/issue/commit' 164 | data = { 165 | "jwt": jwt, 166 | "chapter_id": self.chapter.id, 167 | "title": "issue_title", 168 | "content": "习近平", 169 | "anonymous": 0 170 | } 171 | response = self.client.post(url, data) 172 | self.assertEqual(response.data['message'], "can't save issue! probably have sensitive word!") 173 | return 174 | 175 | def test_follow(self): 176 | jwt = self._student_login_2() 177 | follow_url = '/issue/follow' 178 | follow_check_url = '/issue/follow_check' 179 | data = { 180 | "jwt": jwt, 181 | "issue_id": self.issue.id 182 | } 183 | response = self.client.post(follow_url, data) 184 | self.assertEqual(response.data['code'], 0) 185 | response = self.client.post(follow_check_url, data) 186 | self.assertEqual(response.data['code'], 0) 187 | self.assertEqual(response.data['data']['is_follow'], 1) 188 | return 189 | 190 | def test_favorite(self): 191 | jwt = self._student_login_2() 192 | favorite_url = '/issue/favorite' 193 | like_url = '/issue/like' 194 | data = { 195 | "jwt": jwt, 196 | "issue_id": self.issue.id 197 | } 198 | response = self.client.post(favorite_url, data) 199 | self.assertEqual(response.data['code'], 0) 200 | self.assertEqual(response.data['data']['is_like'], 0) 201 | response = self.client.post(like_url, data) 202 | self.assertEqual(response.data['code'], 0) 203 | self.assertEqual(response.data['data']['is_like'], 1) 204 | response = self.client.post(favorite_url, data) 205 | self.assertEqual(response.data['code'], 0) 206 | self.assertEqual(response.data['data']['is_like'], 1) 207 | return 208 | 209 | def test_issue_update(self): 210 | jwt = self._student_login() 211 | url = '/issue/update' 212 | data = { 213 | "jwt": jwt, 214 | "issue_id": self.issue.id, 215 | "chapter_id": self.chapter.id, 216 | "title": "test_update", 217 | "content": "测试更新content", 218 | "anonymous": 1 219 | } 220 | response = self.client.post(url, data) 221 | self.assertEqual(response.data['code'], 0) 222 | response = self.client.post('/issue/get', {"jwt": jwt, "issue_id": self.issue.id}) 223 | self.assertEqual(response.data['code'], 0) 224 | return 225 | 226 | def test_issue_search(self): 227 | jwt = self._student_login() 228 | url = '/issue/' 229 | data = { 230 | "jwt": jwt, 231 | "keyword": "数学分析级数问题", 232 | "tag_list": [self.tag_1.id], 233 | "status_list": [], 234 | "chapter_list": None, 235 | "subject_id": None, 236 | "year_id": self.year.id, 237 | "order": 3, 238 | "page_no": 1, 239 | "issue_per_page": 100 240 | } 241 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 242 | self.assertEqual(response.data['code'], 0) 243 | return 244 | 245 | def test_issue_tags(self): 246 | jwt = self._student_login() 247 | url = '/issue/tags' 248 | data = { 249 | "jwt": jwt, 250 | "issue_id": self.issue.id 251 | } 252 | response = self.client.post(url, data) 253 | self.assertEqual(response.data['code'], 0) 254 | return 255 | 256 | def test_issue_tags_update(self): 257 | jwt = self._tutor_login_1() 258 | url = '/issue/tags_update' 259 | data = { 260 | "jwt": jwt, 261 | "issue_id": self.issue.id, 262 | "tag_list": [self.tag_1.id, self.tag_2.id] 263 | } 264 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 265 | self.assertEqual(response.data['code'], 0) 266 | response = self.client.post('/issue/tags', {"jwt": jwt, "issue_id": self.issue.id}) 267 | self.assertEqual(response.data['code'], 0) 268 | return 269 | 270 | def test_comment_list(self): 271 | jwt = self._student_login() 272 | url = '/issue/comments' 273 | data = { 274 | "jwt": jwt, 275 | "issue_id": self.issue.id 276 | } 277 | response = self.client.post(url, data) 278 | self.assertEqual(response.data['code'], 0) 279 | return 280 | 281 | def test_comment_create(self): 282 | jwt = self._tutor_login_1() 283 | url = '/issue/comment/create' 284 | data = { 285 | "jwt": jwt, 286 | "issue_id": self.issue.id, 287 | "content": "content_create" 288 | } 289 | response = self.client.post(url, data) 290 | self.assertEqual(response.data['code'], 0) 291 | return 292 | 293 | def test_comment_update(self): 294 | jwt = self._student_login() 295 | url = '/issue/comment/update' 296 | data = { 297 | "jwt": jwt, 298 | "comment_id": self.comment.id, 299 | "content": "content_sexy" 300 | } 301 | response = self.client.post(url, data) 302 | self.assertEqual(response.data['code'], 0) 303 | return 304 | 305 | def test_comment_delete(self): 306 | jwt = self._student_login() 307 | url = '/issue/comment' 308 | data = { 309 | "jwt": jwt, 310 | "comment_id": self.comment.id, 311 | } 312 | response = self.client.delete(url, data) 313 | self.assertEqual(response.data['code'], 0) 314 | return 315 | 316 | def test_draft(self): 317 | jwt = self._student_login() 318 | url = '/issue/save_draft' 319 | data = { 320 | "jwt": jwt, 321 | "chapter_id": None, 322 | "title": "草稿测试", 323 | "content": None, 324 | "anonymous": None 325 | } 326 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 327 | self.assertEqual(response.data['code'], 0) 328 | 329 | url = '/issue/load_draft' 330 | data = { 331 | "jwt": jwt, 332 | } 333 | response = self.client.post(url, data=json.dumps(data), content_type='application/json') 334 | self.assertEqual(response.data['code'], 0) 335 | 336 | def test_associate(self): 337 | jwt = self._admin_login() 338 | add_url = '/issue/associate' 339 | get_url = '/issue/associate/get' 340 | delete_url = '/issue/associate/delete' 341 | add_data = { 342 | "jwt": jwt, 343 | "issue_id": self.issue.id, 344 | "issue_associate_id": self.issue_2.id 345 | } 346 | get_data = { 347 | "jwt": jwt, 348 | "issue_id": self.issue.id 349 | } 350 | delete_data = { 351 | "jwt": jwt, 352 | "issue_id": self.issue.id, 353 | "issue_associate_id": self.issue_2.id 354 | } 355 | response = self.client.post(add_url, data=json.dumps(add_data), content_type='application/json') 356 | self.assertEqual(response.data['code'], 0) 357 | response = self.client.post(add_url, data=json.dumps(add_data), content_type='application/json') 358 | self.assertEqual(response.data['code'], 609) 359 | response = self.client.post(get_url, data=json.dumps(get_data), content_type='application/json') 360 | self.assertEqual(response.data['code'], 0) 361 | response = self.client.post(delete_url, data=json.dumps(delete_data), content_type='application/json') 362 | self.assertEqual(response.data['code'], 0) 363 | response = self.client.post(get_url, data=json.dumps(get_data), content_type='application/json') 364 | self.assertEqual(response.data['code'], 0) 365 | -------------------------------------------------------------------------------- /service_backend/apps/users/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count 2 | from datetime import datetime 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | 6 | from service_backend.apps.users.models import User, BlackList 7 | from service_backend.apps.subjects.models import UserSubject, Subject 8 | from service_backend.apps.issues.models import ReviewIssues, FollowIssues, LikeIssues, AdoptIssues, Issue 9 | from service_backend.apps.utils.views import response_json, encode_password, generate_jwt, check_role 10 | from service_backend.apps.utils.constants import UserErrorCode, SubjectErrorCode, IssueErrorCode, UserRole, IssueStatus, \ 11 | OtherErrorCode 12 | 13 | top_k_max = 10 14 | 15 | 16 | # Create your views here. 17 | def _issue_list_to_json(issue_list): 18 | return \ 19 | {'issue_list': [ 20 | { 21 | 'issue_id': issue.id, 22 | 'create_at': str(issue.created_at), 23 | 'update_at': str(issue.updated_at), 24 | 'title': issue.title, 25 | 'content': issue.content, 26 | 'user_id': issue.user_id, 27 | 'chapter_id': issue.chapter_id, 28 | 'chapter_name': issue.chapter.name, 29 | 'subject_id': issue.chapter.subject_id, 30 | 'subject_name': issue.chapter.subject.name, 31 | 'status': issue.status, 32 | 'anonymous': issue.anonymous, 33 | 'score': issue.score, 34 | 'user_name': issue.user.name 35 | } 36 | for issue in issue_list 37 | ] 38 | } 39 | 40 | 41 | def cal_popular(): 42 | pass 43 | 44 | 45 | class UserLogin(APIView): 46 | def post(self, request): 47 | # print(request.data) 48 | # get user 49 | try: 50 | # if no invalid id, User.objects.get will raise exception 51 | user = User.objects.get(student_id=request.data['student_id']) 52 | except Exception as _e: 53 | return Response( 54 | response_json( 55 | success=False, 56 | code=UserErrorCode.USER_NOT_FOUND, 57 | message='user not found!' 58 | ) 59 | ) 60 | # check password 61 | try: 62 | password_digest = encode_password(request.data['password']) 63 | if password_digest != user.password_digest: 64 | raise Exception() 65 | jwt_token = generate_jwt(user.id) 66 | # print(jwt_token) 67 | except Exception as _e: 68 | return Response(response_json( 69 | success=False, 70 | code=UserErrorCode.INCORRECT_PASSWORD, 71 | message='incorrect password!' 72 | )) 73 | # login success 74 | return Response( 75 | response_json( 76 | success=True, 77 | message='login success!', 78 | data={ 79 | 'jwt': jwt_token, 80 | 'role': user.user_role 81 | } 82 | ) 83 | ) 84 | 85 | 86 | class UserLogout(APIView): 87 | @check_role(UserRole.ALL_USERS) 88 | def post(self, request, action_user: User = None): 89 | expired_token = BlackList(token=request.data['jwt']) 90 | expired_token.save() 91 | return Response(response_json( 92 | success=True, 93 | message="user logout successfully!" 94 | )) 95 | 96 | 97 | class PasswordModify(APIView): 98 | 99 | @check_role(UserRole.ALL_USERS) 100 | def post(self, request, action_user: User = None): 101 | if action_user.password_digest != encode_password(request.data['password_old']): 102 | return Response(response_json( 103 | success=False, 104 | code=UserErrorCode.INCORRECT_PASSWORD, 105 | message='incorrect password!' 106 | )) 107 | try: 108 | action_user.password_digest = encode_password(request.data['password_new']) 109 | action_user.save() 110 | except Exception as _e: 111 | return Response(response_json( 112 | success=False, 113 | code=UserErrorCode.USER_SAVE_FAILED, 114 | message="can't save user!" 115 | )) 116 | return Response(response_json( 117 | success=True, 118 | message="modify password successfully!" 119 | )) 120 | 121 | 122 | class GetUserInfo(APIView): 123 | 124 | @check_role(UserRole.ALL_USERS) 125 | def post(self, request, action_user: User = None): 126 | return Response(response_json( 127 | success=True, 128 | message='get user information successfully!', 129 | data={ 130 | 'user_id': action_user.id, 131 | 'student_id': action_user.student_id, 132 | 'name': action_user.name, 133 | 'mail': action_user.mail, 134 | 'avatar': action_user.avatar, 135 | 'role': action_user.user_role 136 | } 137 | )) 138 | 139 | 140 | class ModifyUserInfo(APIView): 141 | 142 | @check_role(UserRole.ALL_USERS) 143 | def post(self, request, action_user: User = None): 144 | try: 145 | action_user.avatar = request.data['avatar'] 146 | action_user.mail = request.data['mail'] 147 | action_user.save() 148 | except Exception as _e: 149 | return Response(response_json( 150 | success=False, 151 | code=UserErrorCode.USER_SAVE_FAILED, 152 | message="can't save user!" 153 | )) 154 | return Response(response_json( 155 | success=True, 156 | message="modify user information successfully!" 157 | )) 158 | 159 | 160 | class GetUserSubject(APIView): 161 | 162 | @check_role(UserRole.ALL_USERS) 163 | def post(self, request, action_user: User = None): 164 | # check tutor id 165 | try: 166 | tutor_id = request.data['tutor_id'] 167 | tutor = User.objects.get(id=tutor_id) 168 | except Exception as _e: 169 | return Response(response_json( 170 | success=False, 171 | code=UserErrorCode.USER_NOT_FOUND, 172 | message='user not found!', 173 | )) 174 | # get subject list 175 | try: 176 | subject_id_set = {user_subject.subject.id for user_subject in UserSubject.objects.filter(user_id=tutor_id)} 177 | subject_list = [Subject.objects.get(id=subject_id) for subject_id in subject_id_set] 178 | except: 179 | return Response(response_json( 180 | success=False, 181 | code=SubjectErrorCode.SUBJECT_DOES_NOT_EXIST, 182 | message='subject not found!', 183 | )) 184 | # return 185 | return Response((response_json( 186 | success=True, 187 | message="get tutor's subjects successfully!", 188 | data={ 189 | 'subject_list': [ 190 | { 191 | 'subject_id': subject.id, 192 | 'subject_name': subject.name 193 | } 194 | for subject in subject_list 195 | ] 196 | } 197 | ))) 198 | 199 | 200 | class ModifyUserSubject(APIView): 201 | 202 | @check_role(UserRole.ALL_USERS) 203 | def post(self, request, action_user: User = None): 204 | tutor_student_id, subject_id_list = request.data['tutor_id'], request.data['subject_id_list'] 205 | # print(tutor_student_id) 206 | try: 207 | user = User.objects.get(student_id=tutor_student_id) 208 | tutor_id = user.id 209 | UserSubject.objects.filter(user_id=tutor_id).delete() 210 | user_subject_list = [UserSubject(user_id=tutor_id, subject_id=subject_id) 211 | for subject_id in subject_id_list] 212 | UserSubject.objects.bulk_create(user_subject_list) 213 | except Exception as _e: 214 | return Response(response_json( 215 | success=False, 216 | code=SubjectErrorCode.SUBJECT_LIST_UPDATE_FAILED, 217 | message='subject list update failed' 218 | )) 219 | return Response(response_json( 220 | success=True, 221 | message='subject list update successfully!' 222 | )) 223 | 224 | 225 | class CheckUserSubject(APIView): 226 | 227 | @check_role(UserRole.ALL_USERS) 228 | def post(self, request, action_user: User = None): 229 | tutor_id, subject_id = request.data['tutor_id'], request.data['subject_id'] 230 | result = 1 if UserSubject.objects.filter(user_id=tutor_id, subject_id=subject_id).exists() else 0 231 | return Response(response_json( 232 | success=True, 233 | message='user is a tutor of this subject!' if result else 'user is not a tutor of this subject!', 234 | data={ 235 | 'result': result 236 | } 237 | )) 238 | 239 | 240 | class GetReviewIssue(APIView): 241 | 242 | @check_role(UserRole.ALL_USERS) 243 | def post(self, request, action_user: User = None): 244 | page_no, issue_per_page = request.data['page_no'], request.data['issue_per_page'] 245 | try: 246 | issue_list = Issue.objects.filter(review_issues__user_id=action_user.id).order_by( 247 | '-updated_at').distinct() 248 | except Exception as _e: 249 | return Response(response_json( 250 | success=False, 251 | code=IssueErrorCode.REVIEW_ISSUE_QUERY_FAILED, 252 | message="query review issue failed!" 253 | )) 254 | issue_list = issue_list[(page_no - 1) * issue_per_page: page_no * issue_per_page].select_related( 255 | 'chapter').select_related('chapter__subject') 256 | return Response(response_json( 257 | success=True, 258 | message="query review issue successfully!", 259 | data=_issue_list_to_json(issue_list) 260 | )) 261 | 262 | 263 | class GetAdoptIssue(APIView): 264 | 265 | @check_role(UserRole.ALL_USERS) 266 | def post(self, request, action_user: User = None): 267 | page_no, issue_per_page = request.data['page_no'], request.data['issue_per_page'] 268 | try: 269 | issue_list = Issue.objects.filter(adopt_issues__user_id=action_user.id).order_by( 270 | '-updated_at').distinct() 271 | except Exception as _e: 272 | return Response(response_json( 273 | success=False, 274 | code=IssueErrorCode.ADOPT_ISSUE_QUERY_FAILED, 275 | message="query adopt issue failed!" 276 | )) 277 | issue_list = issue_list[(page_no - 1) * issue_per_page: page_no * issue_per_page] 278 | return Response(response_json( 279 | success=True, 280 | message="query adopt issue successfully!", 281 | data=_issue_list_to_json(issue_list) 282 | )) 283 | 284 | 285 | class GetFollowIssue(APIView): 286 | 287 | @check_role(UserRole.ALL_USERS) 288 | def post(self, request, action_user: User = None): 289 | page_no, issue_per_page = request.data['page_no'], request.data['issue_per_page'] 290 | try: 291 | issue_list = Issue.objects.filter(follow_issues__user_id=action_user.id).order_by( 292 | '-updated_at').distinct() 293 | except Exception as _e: 294 | return Response(response_json( 295 | success=False, 296 | code=IssueErrorCode.FOLLOW_ISSUE_QUERY_FAILED, 297 | message="query follow issue failed!" 298 | )) 299 | issue_list = issue_list[(page_no - 1) * issue_per_page: page_no * issue_per_page] 300 | return Response(response_json( 301 | success=True, 302 | message="query follow issue successfully!", 303 | data=_issue_list_to_json(issue_list) 304 | )) 305 | 306 | 307 | class GetAskIssue(APIView): 308 | 309 | @check_role(UserRole.ALL_USERS) 310 | def post(self, request, action_user: User = None): 311 | page_no, issue_per_page = request.data['page_no'], request.data['issue_per_page'] 312 | try: 313 | issue_list = Issue.objects.filter(user_id=action_user.id).order_by( 314 | '-updated_at').distinct() 315 | except Exception as _e: 316 | return Response(response_json( 317 | success=False, 318 | code=IssueErrorCode.ASK_ISSUE_QUERY_FAILED, 319 | message="query ask issue failed!" 320 | )) 321 | issue_list = issue_list[(page_no - 1) * issue_per_page: page_no * issue_per_page] 322 | return Response(response_json( 323 | success=True, 324 | message="query ask issue successfully!", 325 | data=_issue_list_to_json(issue_list) 326 | )) 327 | 328 | 329 | class GetPopularIssue(APIView): 330 | 331 | @check_role(UserRole.ALL_USERS) 332 | def post(self, request, action_user: User = None): 333 | # 选择前k个有效的issue, 按照likes数量降序排列 334 | # 可选项last_week:展示最近一周(按照review_at的时间判断)的popular issue 335 | # TODO: 可以把week扩展到month以及all time 336 | last_week = False 337 | top_k = request.data['top_k'] 338 | if top_k > top_k_max: 339 | return Response(response_json( 340 | success=False, 341 | code=OtherErrorCode.TOO_LARGE_TOPK, 342 | message='expect top_k no more than 10!' 343 | )) 344 | issue_list = Issue.objects.all().filter(status=IssueStatus.VALID_ISSUE).order_by('-likes') 345 | if last_week: 346 | issue_list = [issue for issue in issue_list if 347 | (datetime.now() - datetime.fromisoformat(issue.review_at)).seconds < 7 * 24 * 3600] 348 | issue_list = issue_list[:top_k] 349 | return Response(response_json( 350 | success=True, 351 | data={ 352 | "issue_list": [ 353 | { 354 | 'issue_id': issue.id, 355 | 'create_at': str(issue.created_at), 356 | 'update_at': str(issue.updated_at), 357 | 'title': issue.title, 358 | 'content': issue.content, 359 | 'user_id': issue.user_id, 360 | 'user_name': issue.user.name, 361 | 'user_avatar': issue.user.avatar, 362 | 'counselor_id': issue.counselor_id, 363 | 'counsel_at': str(issue.counsel_at), 364 | 'reviewer_id': issue.reviewer_id, 365 | 'reviewer_at': issue.review_at, 366 | 'chapter_id': issue.chapter_id, 367 | 'chapter_name': issue.chapter.name, 368 | 'subject_id': issue.chapter.subject_id, 369 | 'subject_name': issue.chapter.subject.name, 370 | 'status': issue.status, 371 | 'anonymous': issue.anonymous, 372 | 'score': issue.score, 373 | 'like_count': issue.likes, 374 | 'follow_count': issue.follows 375 | } for issue in issue_list] 376 | } 377 | )) 378 | 379 | 380 | class GetActiveUser(APIView): 381 | @check_role(UserRole.ALL_USERS) 382 | def post(self, request, action_user: User = None): 383 | try: 384 | top_k = request.data['top_k'] 385 | if top_k > top_k_max: 386 | return Response(response_json( 387 | success=False, 388 | code=OtherErrorCode.TOO_LARGE_TOPK, 389 | message='expect top_k no more than 10!' 390 | )) 391 | user_list = User.objects.all().annotate(total_issue=Count('user_issues')).order_by('-total_issue') 392 | user_list = user_list[:top_k] 393 | except Exception as _e: 394 | return Response(response_json( 395 | success=False, 396 | code=UserErrorCode.USER_LOAD_FAILED, 397 | message="can't get user list!" 398 | )) 399 | return Response(response_json( 400 | success=True, 401 | message="get active user successfully!", 402 | data={ 403 | "user_list": [{ 404 | "user_id": user.id, 405 | "student_id": user.student_id, 406 | "name": user.name, 407 | "user_role": user.user_role, 408 | "frozen": user.frozen, 409 | "avatar": user.avatar 410 | } for user in user_list] 411 | } 412 | )) 413 | -------------------------------------------------------------------------------- /service_backend/apps/admins/views.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime, timedelta, time 2 | from math import ceil 3 | 4 | from django.db.models import Q, Count, FloatField, F, Sum, Max, Min 5 | from django.db.models.functions import Cast 6 | from rest_framework.response import Response 7 | from rest_framework.views import APIView 8 | from service_backend.apps.users.models import User 9 | from service_backend.apps.issues.models import Issue, IssueApiCall 10 | from service_backend.apps.utils.views import response_json, encode_password, check_role 11 | from service_backend.apps.utils.constants import UserErrorCode, UserRole, OtherErrorCode, IssueErrorCode, \ 12 | DEFAULT_AVATAR, StatisticsErrorCode 13 | 14 | 15 | def _upper(x:float): 16 | return float(ceil(2.0 * x)) / 2 17 | 18 | 19 | # Create your views here. 20 | class CreateUser(APIView): 21 | 22 | @check_role(UserRole.ADMIN_ONLY) 23 | def post(self, request, action_user: User = None): 24 | # create user 25 | # print(request.data) 26 | student_id, name, password, role = request.data['student_id'], request.data['name'], request.data[ 27 | 'password'], request.data['role'] 28 | try: 29 | if User.objects.filter(student_id=student_id).exists(): 30 | user = User.objects.get(student_id=student_id) 31 | user.name, user.password_digest, user.user_role = name, encode_password(password), role 32 | user.mail = user.mail if user.mail else f"{student_id}@buaa.edu.cn" 33 | user.avatar = user.avatar if user.avatar else DEFAULT_AVATAR 34 | else: 35 | user = User(student_id=student_id, name=name, password_digest=encode_password(password), user_role=role, 36 | mail=f"{student_id}@buaa.edu.cn", avatar=DEFAULT_AVATAR) 37 | user.save() 38 | except Exception as _e: 39 | return Response(response_json( 40 | success=False, 41 | code=UserErrorCode.USER_SAVE_FAILED, 42 | message="can't save user!" 43 | )) 44 | # success 45 | return Response(response_json( 46 | success=True, 47 | message="create user successfully!" 48 | )) 49 | 50 | 51 | class CreateUserBatch(APIView): 52 | 53 | @check_role(UserRole.ADMIN_ONLY) 54 | def post(self, request, action_user: User = None): 55 | # print('user_id is:' + str(user_id)) 56 | # preprocess list 57 | name_list = request.data['name_list'] 58 | student_id_list = request.data['student_id_list'] 59 | password_list = request.data['password_list'] 60 | role_list = request.data['role_list'] 61 | if len({len(name_list), len(student_id_list), len(password_list), len(role_list)}) != 1: 62 | return Response(response_json( 63 | success=False, 64 | code=OtherErrorCode.UNEXPECTED_JSON_FORMAT, 65 | message="inequal list length!" 66 | )) 67 | # iterate 68 | for name, student_id, password, role in zip(name_list, student_id_list, password_list, role_list): 69 | # create user 70 | try: 71 | if User.objects.filter(student_id=student_id).exists(): 72 | user = User.objects.get(student_id=student_id) 73 | user.name, user.password_digest, user.user_role = name, encode_password(password), role 74 | user.mail = user.mail if user.mail else f"{student_id}@buaa.edu.cn" 75 | user.avatar = user.avatar if user.avatar else DEFAULT_AVATAR 76 | else: 77 | user = User(student_id=student_id, name=name, password_digest=encode_password(password), 78 | user_role=role, mail=f"{student_id}@buaa.edu.cn", avatar=DEFAULT_AVATAR) 79 | user.save() 80 | except Exception as _e: 81 | return Response(response_json( 82 | success=False, 83 | code=UserErrorCode.USER_SAVE_FAILED, 84 | message="can't save user!" 85 | )) 86 | # success 87 | return Response(response_json( 88 | success=True, 89 | message="batch create user successfully!" 90 | )) 91 | 92 | 93 | class UserList(APIView): 94 | 95 | @check_role(UserRole.ADMIN_ONLY) 96 | def post(self, request, action_user: User = None): 97 | user_list = User.objects.all() 98 | # user_serializar = UserSerializer(user, many=True) 99 | return Response(response_json( 100 | success=True, 101 | message="get user list successfully!", 102 | data={ 103 | 'user_list': [ 104 | { 105 | 'user_id': user.id, 106 | 'student_id': user.student_id, 107 | 'name': user.name, 108 | 'user_role': user.user_role, 109 | 'frozen': user.frozen 110 | } 111 | for user in user_list 112 | ] 113 | } 114 | )) 115 | 116 | 117 | class UpdateUserRole(APIView): 118 | @check_role(UserRole.ADMIN_ONLY) 119 | def post(self, request, action_user: User = None): 120 | # get user 121 | try: 122 | user = User.objects.get(id=request.data['user_id']) 123 | except Exception as _e: 124 | return Response(response_json( 125 | success=False, 126 | code=UserErrorCode.USER_NOT_FOUND, 127 | message="can't find user!" 128 | )) 129 | # modify user role 130 | user.user_role = request.data['user_role'] 131 | # save user role 132 | try: 133 | user.save() 134 | except Exception as _e: 135 | return Response(response_json( 136 | success=False, 137 | code=UserErrorCode.USER_SAVE_FAILED, 138 | message="can't save user!" 139 | )) 140 | return Response(response_json( 141 | success=True, 142 | message="update user role successfully!" 143 | )) 144 | 145 | 146 | class FreezeUser(APIView): 147 | @check_role(UserRole.ADMIN_ONLY) 148 | def post(self, request, action_user: User = None): 149 | # get user 150 | try: 151 | user = User.objects.get(id=request.data['user_id']) 152 | except Exception as _e: 153 | return Response(response_json( 154 | success=False, 155 | code=UserErrorCode.USER_NOT_FOUND, 156 | message="can't find user!" 157 | )) 158 | # modify frozen status 159 | user.frozen = request.data['frozen'] 160 | # save frozen status 161 | try: 162 | user.save() 163 | except Exception as _e: 164 | return Response(response_json( 165 | success=False, 166 | code=UserErrorCode.USER_SAVE_FAILED, 167 | message="can't save user!" 168 | )) 169 | return Response(response_json( 170 | success=True, 171 | message="update frozen status successfully!" 172 | )) 173 | 174 | 175 | class DeleteIssue(APIView): 176 | 177 | @check_role(UserRole.ADMIN_ONLY) 178 | def post(self, request, action_user: User = None): 179 | try: 180 | issue = Issue.objects.get(id=request.data['issue_id']) 181 | except Exception as _e: 182 | return Response(response_json( 183 | success=False, 184 | code=IssueErrorCode.ISSUE_NOT_FOUND, 185 | message="can't find issue!" 186 | )) 187 | try: 188 | issue.delete() 189 | except Exception as _e: 190 | return Response(response_json( 191 | success=False, 192 | code=IssueErrorCode.ISSUE_DELETE_FAILED, 193 | message="can't delete issue!" 194 | )) 195 | return Response(response_json( 196 | success=True, 197 | message="delete issue successfully!" 198 | )) 199 | 200 | 201 | class GetStatistics(APIView): 202 | 203 | @check_role(UserRole.ADMIN_ONLY) 204 | def post(self, request, action_user: User = None): 205 | tp, indicator = request.data['type'], request.data['indicator'] 206 | begin_date, end_date = date.fromisoformat(str(request.data['begin_date']).strip()), date.fromisoformat( 207 | str(request.data['end_date']).strip()) 208 | begin_datetime, end_datetime = datetime.combine(begin_date, time.min), datetime.combine(end_date, time.max) 209 | res_list = [] 210 | if indicator == 0: 211 | issue_query_set = Issue.objects.all().filter( 212 | Q(counsel_at__gte=begin_datetime) & Q(counsel_at__lte=end_datetime)) 213 | if tp == 0: 214 | cur_date = begin_date 215 | while cur_date <= end_date: 216 | cnt = issue_query_set.filter(Q(counsel_at__gte=datetime.combine(cur_date, time.min)) & Q( 217 | counsel_at__lte=datetime.combine(cur_date, time.max))).count() 218 | res_list.append(cnt) 219 | cur_date += timedelta(days=1) 220 | else: 221 | count_list = issue_query_set.values('counselor_id').annotate(count=Count('counselor_id')) 222 | res_list = [cnt['count'] for cnt in count_list] 223 | elif indicator == 1: 224 | issue_query_set = Issue.objects.all().filter( 225 | Q(review_at__gte=begin_datetime) & Q(review_at__lte=end_datetime)) 226 | if tp == 0: 227 | cur_date = begin_date 228 | while cur_date <= end_date: 229 | cnt = issue_query_set.filter(Q(review_at__gte=datetime.combine(cur_date, time.min)) & Q( 230 | review_at__lte=datetime.combine(cur_date, time.max))).count() 231 | res_list.append(cnt) 232 | cur_date += timedelta(days=1) 233 | else: 234 | count_list = issue_query_set.values('reviewer_id').annotate(count=Count('reviewer_id')) 235 | res_list = [cnt['count'] for cnt in count_list] 236 | elif indicator == 2: 237 | api_call_query_set = IssueApiCall.objects.all().filter( 238 | Q(created_at__gte=begin_datetime) & Q(created_at__lte=end_datetime)) 239 | if tp == 0: 240 | cur_date = begin_date 241 | while cur_date <= end_date: 242 | cnt = api_call_query_set.filter(Q(created_at__gte=datetime.combine(cur_date, time.min)) & Q( 243 | created_at__lte=datetime.combine(cur_date, time.max))).count() 244 | res_list.append(cnt) 245 | cur_date += timedelta(days=1) 246 | else: 247 | count_list = api_call_query_set.values('issue_id').annotate(count=Count('issue_id')) 248 | res_list = [cnt['count'] for cnt in count_list] 249 | else: 250 | pass 251 | # res_list = sorted(res_list) 252 | return Response(response_json( 253 | success=True, 254 | message="get statistics successfully!", 255 | data={ 256 | 'list': res_list 257 | } 258 | )) 259 | 260 | 261 | class GetTutorBonus(APIView): 262 | 263 | @check_role(UserRole.ADMIN_ONLY) 264 | def post(self, request, action_user: User = None): 265 | bonus_per_counsel, bonus_per_review = float(request.data['bonus_per_counsel']), float( 266 | request.data['bonus_per_review']) 267 | begin_date, end_date = date.fromisoformat(str(request.data['begin_date']).strip()), date.fromisoformat( 268 | str(request.data['end_date']).strip()) 269 | begin_datetime, end_datetime = datetime.combine(begin_date, time.min), datetime.combine(end_date, time.max) 270 | min_bonus, max_bonus = float(request.data['min_bonus']), float(request.data['max_bonus']) 271 | # statistic 272 | counsel_issue = Issue.objects.all().filter(Q(counsel_at__gte=begin_datetime) & Q(counsel_at__lte=end_datetime)) 273 | review_issue = Issue.objects.all().filter(Q(review_at__gte=begin_datetime) & Q(review_at__lte=end_datetime)) 274 | counsel_count = counsel_issue.values('counselor_id').annotate(int_count=Count('counselor_id')) 275 | review_count = review_issue.values('reviewer_id').annotate(int_count=Count('reviewer_id')) 276 | value_dict = dict() 277 | for item in counsel_count: 278 | if item['counselor_id'] in value_dict: 279 | value_dict[item['counselor_id']] += item['int_count'] * bonus_per_counsel 280 | else: 281 | value_dict[item['counselor_id']] = item['int_count'] * bonus_per_counsel 282 | for item in review_count: 283 | if item['reviewer_id'] in value_dict: 284 | value_dict[item['reviewer_id']] += item['int_count'] * bonus_per_review 285 | else: 286 | value_dict[item['reviewer_id']] = item['int_count'] * bonus_per_review 287 | max_value, min_value = max(value_dict.values()), min(value_dict.values()) 288 | # linear_projection = max_value > max_bonus or min_value < min_bonus 289 | # linear_projection = linear_projection and (bonus_per_counsel >= 0.0 and bonus_per_review >= 0.0 and 0.0 <= min_bonus < max_bonus) 290 | linear_projection = False 291 | if linear_projection: 292 | total_bonus = [{'id': k, 'bonus': (v - min_value) * (max_bonus - min_bonus) / (max_value - min_value) + min_bonus} for k, v in value_dict.items()] 293 | else: 294 | total_bonus = [{'id': k, 'bonus': v} for k, v in value_dict.items()] 295 | # to list 296 | res_list = [] 297 | for item in total_bonus: 298 | user = User.objects.get(id=item['id']) 299 | res_list.append( 300 | { 301 | "id": item["id"], 302 | "student_id": user.student_id, 303 | "name": user.name, 304 | "bonus": _upper(item["bonus"]) 305 | } 306 | ) 307 | return Response(response_json( 308 | success=True, 309 | message="get bonus list successfully!", 310 | data={ 311 | 'bonus_list': res_list 312 | } 313 | )) 314 | 315 | 316 | class GetStudentBonus(APIView): 317 | 318 | @check_role(UserRole.ADMIN_ONLY) 319 | def post(self, request, action_user: User = None): 320 | bonus_per_issue = float(request.data['bonus_per_issue']) 321 | begin_date, end_date = date.fromisoformat(str(request.data['begin_date']).strip()), date.fromisoformat( 322 | str(request.data['end_date']).strip()) 323 | begin_datetime, end_datetime = datetime.combine(begin_date, time.min), datetime.combine(end_date, time.max) 324 | min_bonus, max_bonus = float(request.data['min_bonus']), float(request.data['max_bonus']) 325 | # statistic 326 | issue_query_set = Issue.objects.all().filter(Q(created_at__gte=begin_datetime) & Q(created_at__lte=end_datetime)) 327 | issue_count = issue_query_set.values('user_id').annotate(int_count=Count('user_id')) 328 | issue_count = issue_count.annotate(float_count=Cast('int_count', FloatField())) 329 | issue_count = issue_count.annotate(count=F('float_count') * bonus_per_issue) 330 | issue_count = issue_count.values('user_id', 'count') 331 | issue_count = issue_count.annotate(id=F('user_id')) 332 | total_value = issue_count.annotate(value=F('count')) 333 | total_value = total_value.values('id', 'value') 334 | max_value, min_value = total_value.aggregate(Max('value'))['value__max'], total_value.aggregate(Min('value'))['value__min'] 335 | # if min_value == max_value: 336 | # return Response(response_json( 337 | # success=False, 338 | # code=StatisticsErrorCode.BONUS_ALL_THE_SAME, 339 | # message='volunteer time bonus all the same!' 340 | # )) 341 | # check whether proj 342 | # linear_projection = bonus_per_issue >= 0.0 and 0.0 <= min_bonus < max_bonus 343 | linear_projection = False 344 | if linear_projection: 345 | total_bonus = total_value.annotate(bonus=F('value') * (max_bonus - min_bonus) / (max_value - min_bonus) + min_bonus).values('id', 'bonus') 346 | else: 347 | total_bonus = total_value.annotate(bonus=F('value')).values('id', 'bonus') 348 | # to list 349 | res_list = [] 350 | for item in total_bonus: 351 | user = User.objects.get(id=item['id']) 352 | res_list.append( 353 | { 354 | "id": item["id"], 355 | "student_id": user.student_id, 356 | "name": user.name, 357 | "bonus": _upper(item["bonus"]) 358 | } 359 | ) 360 | return Response(response_json( 361 | success=True, 362 | message="get bonus list successfully!", 363 | data={ 364 | 'bonus_list': res_list 365 | } 366 | )) 367 | --------------------------------------------------------------------------------