cscenter/lms

Переезд ШАД

Closed this issue · 3 comments

  • создать проект для ШАДа, перенести settings и запустить
  • создать отделение для ШАДа
  • проверить, что от ШАДа ничего не появилось на сайтах центра/клуба
  • добавить состояние черновика для прочтения курса
  • дальше идти по вьюхам и смотреть, что могло протечь

Поэтапное тестирование сайтов CS центра/клуба при добавлении контента на сайте ШАДа:

  • Просто создал пустой сайт - могли быть какие-то лишные табы, но ничего не появилось.
  • Добавил прочтение курса только для ШАДа - в Обучении всё ОК, в Курировании на сайте центра доступна ведомость по курсу, также курс в списке для поиска пересечений. Как должно работать Преподавание между сайтами?

MAJOR:

  • когда захожу на сайте ШАД в раздел Курсы с центрового аккаунта, получаю TOO_MANY_REDIRECTS

MINOR:

  • добавить в Полезное какой-то текст на случай, когда в базе пусто.
  • Пофиксить переводы для LMS

SITE SPECIFIC

  • Обучение > Библиотека
  • Набор
  • Проекты
  • Курирование (например, в списке отображаются ведомости всех курсов, но открыть можно только ведомость курсов с текущего сайта)
  • Админка?
  • Статистика

SITE CONFIGURATION

  • лого
  • год основания
  • вынести DEFAULT_BRANCH_CODE из core в настройки сайта

TEMPLATES

  • Computer Science Center -> request.site.name в шаблонах (<title>, img[alt], footer)
  • Computer Science Center в переводах/тексте полей моделек
  • "Кураторы CS центра" в e-mail уведомлении по проектам
  • убрать надписи "CS центр" из шаблонов

to be updated

Скрипт для импорта данных из энитаска.

"""
Import Anytask data to CSC LMS

Minimal set of attributes:
 * MetaCourse: name_ru, slug, description
 * Course: meta_course, grading_type, semester, main_branch, language, material_visibility, teachers, group_mode
 * StudentProfile: type, branch, user, year_of_admission, year_of_curriculum (if present), status
 * Enrollment: user, profile, course, grade
 * StudentGroup: type, name, course

Required data:
 - exports from Anytask
 - info about courses
 - metacourse slugs mapping
 - user mapping to resolve special cases

Plan:
 + extract course teachers and get/create users for them
 + create meta_courses
 + create course and link them to corresponding teachers and meta_courses
 + create student groups for courses
 + create users for students
 + create student profiles and enrollments with filled grades and student groups

TODO:
 - carefully process users with matching first name and last name
 - student profiles for course teachers (?)
 - setup additional branches for courses (?)
 - distinguish Academic 1 & 2 (?)
 - place students into student groups (resolve multiple groups for student in one course)
 - fix skipping of CSC students
 - multiple branches in logs
 - Подчистить инфу об отделениях из фамилий (?)

FIX:
 - Глубокое обучение и _Глубокое обучение объединили - из-за этого IntegrityError('duplicate key value violates
   unique constraint "learning_enrollment_student_id_course_offering_id"\nDETAIL:  Key
   (student_id, course_id)=(7891, 777) already exists.\n')

TEST:
 - check slugs for Основы стохастики and Продукт: создание и управление
 - check that everything is OK with imported 10-point grades (no students that have passed the course were imported)
"""

import json
import logging
from collections import Counter
from datetime import datetime

import pandas as pd
from django.db import IntegrityError
from django.db.models import Q

from core.models import Branch
from core.utils import ru_en_mapping
from courses.constants import MaterialVisibilityTypes
from courses.models import MetaCourse, Course, Semester, StudentGroupTypes
from courses.services import CourseService
from learning.models import Enrollment, StudentGroup
from learning.settings import GradingSystems, GradeTypes, StudentStatuses
from study_programs.models import AcademicDiscipline
from users.constants import GenderTypes, Roles
from users.models import User, StudentProfile, StudentTypes, StudentStatusLog

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

SHAD_COURSES = '/home/kapralovn/work/cscenter/misc/anytask/shad_courses.csv'
SHAD_DATA = '/home/kapralovn/work/cscenter/misc/anytask/shad_data.json'
SHAD_MIGRATOR_USERNAME = 'robot_shad_migrator'

MARKS = {
    'отлично': GradeTypes.EXCELLENT,
    'хорошо': GradeTypes.GOOD,
    'зачет': GradeTypes.CREDIT,
    'незачет': GradeTypes.UNSATISFACTORY,
    '10': GradeTypes.TEN,
    '9': GradeTypes.NINE,
    '8': GradeTypes.EIGHT,
    '7': GradeTypes.SEVEN,
    '6': GradeTypes.SIX,
    '5': GradeTypes.FIVE,
    '4': GradeTypes.FOUR,
    '3': GradeTypes.THREE,
    '2': GradeTypes.TWO,
    '1': GradeTypes.ONE,
    None: GradeTypes.NOT_GRADED
}
USER_STATUS_MAPPING = {
    1: {'status': StudentStatuses.ACADEMIC_LEAVE},
    2: {'status': StudentStatuses.EXPELLED},
    3: {'year_of_curriculum': 2016},
    4: {'status': StudentStatuses.ACADEMIC_LEAVE},
    5: {'branch': 'Заочное отделение'},
    6: {'branch': 'Нижний Новгород'},
    7: {'branch': 'Екатеринбург'},
    8: {'branch': 'Минск'},
    9: {'branch': 'Москва'},
    10: {'type': StudentTypes.VOLUNTEER},
    11: {'academic_disciplines': 'ds'},
    12: {'academic_disciplines': 'cs'},
    13: {'academic_disciplines': 'bd'},
    14: {'year_of_curriculum': 2017},
    15: {'status': StudentStatuses.GRADUATE},
    16: {'year_of_curriculum': 2016},
    17: {'cs_center': True},
    18: {'year_of_curriculum': 2018},
    20: {'academic_disciplines': 'ds'},
    21: {'academic_disciplines': 'ml'},
    22: {'academic_disciplines': 'bd'},
    23: {'year_of_curriculum': 2019},
    24: {'year_of_curriculum': 2019},
    25: {'year_of_curriculum': 2019},
}

COURSE_WITH_TEN_POINT_MARK_SYSTEM = 575
SKIP_COURSE = 133

USER_MAPPING = {
    'pritykovskaya': 586,
    'sobrietate69': 6684,
}
SPECIAL_CHARACTERS_MAPPING = {
    ' ': '-',
    '(': '',
    ')': '',
    '.': '',
    ':': '',
    ',': '',
    '+': 'p',
}
ACADEMIC_DISCIPLINES = [
    dict(code='bd', name='Инфраструктура больших данных'),
    dict(code='ml', name='Разработка машинного обучения')
]
ANYTASK_PROFILE_TEMPLATE = 'https://anytask.org/accounts/profile/{username}'
ADMIN_USER = User.objects.get(pk=1)
# YDS
SITE_ID = 3
DEFAULT_BRANCH_CODE = 'msk'


def anytask_profile_url(user):
    return ANYTASK_PROFILE_TEMPLATE.format(username=user['username'])


def parse_user_status(user_status):
    data = {}
    for status in user_status:
        data.update(USER_STATUS_MAPPING[status])
    return data


def get_or_create_user(user_data, user_mapping=None, check=False):
    """
    Gets or creates user of the CSC website from anytask data.
    Use `user_mapping` to resolve special cases when 2 or more users with the similar credentials exist.

    @param user_mapping: dictionary {anytask_username, csc_user.pk}
    @return user, created
    """
    user_profile = user_data['profile']
    username = user_data['username']
    email = user_data['email']

    # parsed_user_status = parse_user_status(user_profile['user_status'])
    # if 'cs_center' in parsed_user_status:
    #     return None

    if user_mapping and username in user_mapping:
        csc_pk = user_mapping[username]
        return User.objects.get(pk=csc_pk), False

    csc_users = User.objects.filter(Q(username=username) | Q(email=email))

    if csc_users.count() > 1:
        logger.error(f'Cannot create user with username {username} and email {email} - '
                      f'more than one CSC user with the same data already exists')
        return None

    return csc_users.get_or_create(defaults=dict(
        username=username, email=email,
        first_name=user_data['first_name'], last_name=user_data['last_name'],
        patronymic=user_profile['middle_name'] if user_profile['middle_name'] else '',
        gender=GenderTypes.OTHER, anytask_url=anytask_profile_url(user_data),
        time_zone=user_profile['time_zone']
    ))


def get_or_create_meta_course(meta_course_name, metacourse_slug_mapping=None):
    try:
        meta_course = MetaCourse.objects.get(name_ru=meta_course_name)
        return meta_course
    except MetaCourse.DoesNotExist:
        if not metacourse_slug_mapping or meta_course_name not in metacourse_slug_mapping:
            logger.info(f'Cannot create a meta course {meta_course_name}')
            return None
        meta_course_slug = metacourse_slug_mapping[meta_course_name]
        meta_course, _ = MetaCourse.objects.get_or_create(name_ru=meta_course_name, defaults={
            'slug': meta_course_slug, 'description': 'TBD'
        })
        return meta_course


def get_or_create_course(course_data, courses_df, csc_meta_courses, csc_teachers):
    course_id = course_data['id']
    course_info = courses_df.loc[course_id, :]
    course_teachers = course_data['teachers']
    course_groups = course_data['groups']

    meta_course_name = course_info['LMS Course']
    meta_course = csc_meta_courses[meta_course_name]
    semester_type = course_info['Semester Type']
    semester, _ = Semester.objects.get_or_create(year=course_info['Year'], type=semester_type)
    branch = Branch.objects.get(name_ru=course_info['Main Branch'], site_id=SITE_ID)
    grading_type = GradingSystems.BASE if course_id != COURSE_WITH_TEN_POINT_MARK_SYSTEM else GradingSystems.TEN_POINT
    group_mode = StudentGroupTypes.MANUAL if len(course_groups) > 1 else StudentGroupTypes.NO_GROUPS
    course, _ = Course.objects.update_or_create(meta_course=meta_course, semester=semester, main_branch=branch,
                                                defaults=dict(
                                                    grading_type=grading_type, language='ru',
                                                    materials_visibility=MaterialVisibilityTypes.VISIBLE,
                                                    group_mode=group_mode
                                                ))

    # Add `distance` branch to all courses
    distance_branch = Branch.objects.get(code='distance', site_id=SITE_ID)
    course.additional_branches.add(distance_branch)

    # Sync branches
    CourseService.sync_branches(course)

    # Add course teachers
    for course_teacher in course_teachers:
        teacher_username = course_teacher['username']
        # Robot user for shad migration was added as a teacher to all shad courses, skip it
        if teacher_username == SHAD_MIGRATOR_USERNAME:
            continue
        teacher = csc_teachers[teacher_username]
        course.teachers.add(teacher)
    course.save()

    # Add student groups if necessary
    if len(course_groups) > 1:
        for group in course_groups:
            StudentGroup.objects.update_or_create(type=StudentGroupTypes.MANUAL,
                                                  name=group['name'],
                                                  course=course)

    return course


def get_or_create_student_profile(user, user_data, csc_courses, group_course_mapping):
    user_profile = user_data['profile']
    user_profile_logs = user_data['profiles_logs_by_user']
    user_groups = user_data['group_set']

    # Check whether we need to create student profile - user should be in one of the groups that have access to courses
    user_group_ids = set(group['id'] for group in user_groups)
    course_groups = user_group_ids.intersection(group_course_mapping.keys())
    if not course_groups:
        logger.info(f'{user} - no student profile needed')
        return

    # Derive courses for user from groups
    user_courses = set()
    for group in course_groups:
        user_courses |= set(group_course_mapping[group])

    # Gather marks for courses
    user_marks = {}
    for course_mark in user_data['studentcoursemark_set']:
        course_id = course_mark['course']
        if course_id in user_courses:
            mark = course_mark['mark']
            grade = MARKS[mark['name']] if mark else GradeTypes.NOT_GRADED
            user_marks[course_id] = grade
    for course in user_courses:
        if course not in user_marks:
            user_marks[course] = GradeTypes.NOT_GRADED

    # Gather data for student profile
    profile_data = {}

    # Year of admission = year of the earliest group for the student
    earliest_group = sorted(user_groups, key=lambda g: g['year']['start_year'])[0]
    profile_data['year_of_admission'] = earliest_group['year']['start_year']

    # Parse user status - retrieve branch, year_of_curriculum, academic_disciplines, status, type
    profile_data['type'] = StudentTypes.REGULAR
    parsed_user_status = parse_user_status(user_profile['user_status'])
    profile_data.update(parsed_user_status)

    # Skip CSC users
    if 'cs_center' in profile_data:
        logger.info(f'{user} - CSC student: {user.get_absolute_url()}')
        return

    # Retrieve academic discipline from DB if status was found
    # OR Use the most frequent course branch if no status was found, fallback to DEFAULT_BRANCH_CODE
    if 'branch' in profile_data:
        profile_data['branch'] = Branch.objects.get(name=profile_data['branch'], site_id=SITE_ID)
    else:
        course_branches = []
        for course in user_courses:
            course_branches.append(csc_courses[course].main_branch)
        branch_counts = Counter(course_branches)
        max_count = max(branch_counts.values())
        max_count_branches = []
        for branch, count in branch_counts.items():
            if count == max_count:
                max_count_branches.append(branch)
        if len(max_count_branches) == 1:
            profile_data['branch'] = max_count_branches[0]
        else:
            profile_data['branch'] = Branch.objects.get(code=DEFAULT_BRANCH_CODE, site_id=SITE_ID)

    # Update user branch accordingly
    user.branch = profile_data['branch']
    user.save()

    # Retrieve academic discipline from DB
    academic_discipline = None
    if 'academic_disciplines' in profile_data:
        academic_discipline = AcademicDiscipline.objects.get(code=profile_data['academic_disciplines'])
        del profile_data['academic_disciplines']

    # Create student profile
    student_profile, _ = StudentProfile.objects.update_or_create(user=user, **profile_data)
    if academic_discipline:
        student_profile.academic_disciplines.add(academic_discipline)
        student_profile.save()

    # Create status logs for all status changes
    user_profile_logs.append(user_profile)
    last_status_added = None
    for log in user_profile_logs:
        parsed_status = parse_user_status(log['user_status']).get('status', None)
        timestamp = log['update_time']
        date = datetime.fromisoformat(timestamp[:-1]).date()
        if parsed_status != last_status_added:
            StudentStatusLog.objects.get_or_create(status_changed_at=date, status=parsed_status,
                                                   student_profile=student_profile, entry_author=ADMIN_USER)
            last_status_added = parsed_status

    # Add all enrollments with grades
    for course_id, grade in user_marks.items():
        try:
            course = csc_courses[course_id]
            enrollment = Enrollment.objects.update_or_create(student=user, student_profile=student_profile,
                                                             course=course, grade=grade)
            logger.debug(f'{user} - {student_profile} - {enrollment}')
        except IntegrityError as e:
            logger.error(f'{user} - {student_profile} - {repr(e)}')
    return student_profile


def skip_bad_course(courses):
    logger.info(f'Skipping course with id={SKIP_COURSE}')
    return [course for course in courses if course['id'] != SKIP_COURSE]


def extract_course_teachers(courses):
    course_teachers = {}
    for course in courses:
        teachers = course['teachers']
        for teacher in teachers:
            username = teacher['username']
            # Robot user for shad migration was added as a teacher to all shad courses, skip it
            if username == SHAD_MIGRATOR_USERNAME:
                continue
            course_teachers[username] = teacher
    return course_teachers


def check_duplicates(users):
    for user in users:
        found = False
        username = user['username']
        csc_users = User.objects.filter(username=username)
        if csc_users.count() > 0:
            print(f'Users with username {username} already exist!')
            found = True

        email = user['email']
        csc_users = User.objects.filter(email=email)
        if csc_users.count() > 0:
            print(f'Users with email {email} already exist!')
            found = True

        first_name = user['first_name']
        last_name = user['last_name']
        if first_name and last_name:
            patronymic = user['profile']['middle_name']
            if patronymic:
                csc_users = User.objects.filter(first_name=first_name, last_name=last_name, patronymic=patronymic)
            else:
                csc_users = User.objects.filter(first_name=first_name, last_name=last_name)
            if csc_users.count() > 0:
                print(f"User '{first_name} {last_name} {patronymic or ''}' already exists!")
                found = True

        if found:
            print(f'Anytask: {anytask_profile_url(user)} ')
            for csc_user in csc_users:
                print(f'CSC: {csc_user} - {csc_user.get_absolute_url()}')


def create_student_profiles(users, csc_users, csc_courses, group_course_mapping):
    num_created = 0
    for user in users:
        username = user['username']
        csc_user = csc_users[username]
        student_profile = get_or_create_student_profile(csc_user, user, csc_courses, group_course_mapping)
        if student_profile:
            num_created += 1


def create_users(users, user_mapping):
    csc_users = {}
    num_created = 0
    for user in users:
        username = user['username']
        csc_users[username], created = get_or_create_user(user, user_mapping)
        if created:
            num_created += 1
    bad_users = [u for u in csc_users.values() if not u]
    print(f'Done. Got {len(csc_users)} users total. '
          f'Created {num_created} new users, could not create {len(bad_users)} users.')
    return csc_users


def create_meta_courses(meta_courses, metacourse_slug_mapping=None):
    return {meta_course: get_or_create_meta_course(meta_course, metacourse_slug_mapping)
            for meta_course in meta_courses}


def create_courses(courses, courses_df, csc_meta_courses, csc_teachers):
    csc_courses = {}
    for course in courses:
        course_id = course['id']
        csc_courses[course_id] = get_or_create_course(course, courses_df, csc_meta_courses, csc_teachers)
    return csc_courses


def replace_special_characters(slug):
    for key, value in SPECIAL_CHARACTERS_MAPPING.items():
        slug = slug.replace(key, value)
    return slug


def generate_metacourse_slugs(meta_courses):
    return [replace_special_characters(meta_course.lower()[:50].translate(ru_en_mapping))
            for meta_course in meta_courses]


def get_courses_without_mark_system(courses):
    """
    Some old courses do not have a mark system attached.
    """
    courses_without_mark_system = []
    for course in courses:
        if not course['mark_system']:
            courses_without_mark_system.append(course['id'])
    return courses_without_mark_system


def check_group_intersections(courses):
    from itertools import combinations
    for course in courses:
        course_groups = course['groups']
        if len(course_groups) > 1:
            for g1, g2 in combinations(course_groups, 2):
                intersection = set(g1['students']).intersection(set(g2['students']))
                if intersection:
                    print(course['id'], g1['id'], g2['id'], len(intersection), intersection, sep='\t')


def create_academic_disciplines(disciplines):
    for discipline in disciplines:
        AcademicDiscipline.objects.update_or_create(code=discipline['code'], name=discipline['name'])


def get_group_course_mapping(courses):
    group_course_mapping = {}
    for course in courses:
        course_id = course['id']
        for group in course['groups']:
            group_id = group['id']
            if group_id not in group_course_mapping:
                group_course_mapping[group_id] = []
            group_course_mapping[group_id].append(course_id)
    return group_course_mapping


def done():
    logger.info('Done.')


def load_anytask_data():
    # Load CSV with course info
    logger.info('Loading CSV with hand-curated info about courses...')
    courses_df = pd.read_csv(SHAD_COURSES, delimiter=',', skiprows=1,
                             names=['Anytask Course', 'Anytask Year', 'LMS Course', 'Org',
                                    'Main Branch', 'Semester', 'Link', 'ID']).set_index('ID')
    courses_df['Semester'] = courses_df['Semester'].str.split(' ')
    courses_df['Semester Type'] = courses_df['Semester'].apply(
        lambda chunks: 'spring' if chunks[0] == 'весна' else 'autumn')
    courses_df['Year'] = courses_df['Semester'].apply(lambda chunks: int(chunks[1]))
    done()

    # Load data from Anytask
    logger.info('Loading JSON with Anytask data...')
    with open(SHAD_DATA, 'r') as f:
        shad_data = json.load(f)

    users = shad_data['users']
    courses = shad_data['courses']
    courses = skip_bad_course(courses)
    done()

    # Create users for course teachers
    logger.info('Creating LMS users for course teachers...')
    course_teachers = extract_course_teachers(courses)
    csc_teachers = create_users(users=course_teachers.values(), user_mapping=USER_MAPPING)
    assert len(csc_teachers) == len(course_teachers), 'Failed to create course teachers'
    for csc_teacher in csc_teachers.values():
        csc_teacher.add_group(Roles.TEACHER)
    done()

    # Generate slugs for new meta courses
    logger.info('Generating slugs for new meta courses...')
    meta_courses = set(courses_df['LMS Course'].values)
    existing_meta_courses = {mc.name: mc for mc in MetaCourse.objects.filter(name__in=meta_courses)}
    new_meta_courses = [mc for mc in meta_courses if mc not in existing_meta_courses.keys()]

    metacourse_slugs = generate_metacourse_slugs(new_meta_courses)
    metacourse_slug_mapping = dict(zip(new_meta_courses, metacourse_slugs))
    done()

    # Create metacourses
    logger.info('Creating new meta courses...')
    meta_courses = set(courses_df['LMS Course'].values)
    csc_meta_courses = create_meta_courses(meta_courses, metacourse_slug_mapping)
    assert len(meta_courses) == len(csc_meta_courses), 'Failed to create meta courses'
    done()

    # Create academic disciplines
    logger.info('Creating academic disciplines...')
    create_academic_disciplines(ACADEMIC_DISCIPLINES)
    done()

    # Create courses
    logger.info('Creating courses...')
    csc_courses = create_courses(courses, courses_df, csc_meta_courses, csc_teachers)
    assert len(courses) == len(csc_courses), 'Failed to create courses'
    done()

    # Create users for students
    logger.info('Creating LMS users for students...')
    csc_users = create_users(users, user_mapping=USER_MAPPING)
    assert len(users) == len(csc_users), 'Failed to create users'
    done()

    # Create student profiles and fill grades
    logger.info('Creating student profiles and filling grades...')
    group_course_mapping = get_group_course_mapping(courses)
    create_student_profiles(users, csc_users, csc_courses, group_course_mapping)
    done()

Скрипт для выгрузки данных из энитаска.

from django.contrib.auth.models import User
from rest_framework import serializers
from rest_framework.renderers import JSONRenderer

from courses.models import StudentCourseMark, MarkField, Course, \
    CourseMarkSystem, DefaultTeacher, FilenameExtension
from groups.models import Group
from issues.model_issue_field import IssueField
from issues.model_issue_status import IssueStatus, IssueStatusSystem
from issues.models import Issue, Event, File
from tasks.models import Task, TaskGroupRelations
from users.model_user_status import UserStatus
from users.models import UserProfile, UserProfileLog
from years.models import Year


class YearSerializer(serializers.ModelSerializer):
    class Meta:
        model = Year
        fields = ['start_year']


class GroupSerializer(serializers.ModelSerializer):
    year = YearSerializer()

    class Meta:
        model = Group
        fields = '__all__'


class MarkFieldSerializer(serializers.ModelSerializer):
    class Meta:
        model = MarkField
        fields = '__all__'


class StudentCourseMarkSerializer(serializers.ModelSerializer):
    mark = MarkFieldSerializer()

    class Meta:
        model = StudentCourseMark
        fields = ['course', 'mark']


class UserStatusSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserStatus
        fields = '__all__'


class UserProfileSerializer(serializers.ModelSerializer):
    user_status = UserStatusSerializer(many=True)

    class Meta:
        model = UserProfile
        fields = '__all__'


class UserProfileLogSerializer(serializers.ModelSerializer):
    user_status = UserStatusSerializer(many=True)

    class Meta:
        model = UserProfileLog
        fields = '__all__'


class UserSerializer(serializers.ModelSerializer):
    group_set = GroupSerializer(many=True)
    profile = UserProfileSerializer()
    profiles_logs_by_user = UserProfileLogSerializer(many=True)
    studentcoursemark_set = StudentCourseMarkSerializer(many=True)

    class Meta:
        model = User
        fields = ['username', 'first_name', 'last_name', 'email', 'is_staff',
                  'is_active', 'date_joined',
                  'group_set', 'profile', 'profiles_logs_by_user',
                  'studentcoursemark_set']


class FilenameExtensionSerializer(serializers.ModelSerializer):
    class Meta:
        model = FilenameExtension
        fields = '__all__'


class CourseMarkSystemSerializer(serializers.ModelSerializer):
    marks = MarkFieldSerializer(many=True)

    class Meta:
        model = CourseMarkSystem
        fields = '__all__'


class IssueStatusSerializer(serializers.ModelSerializer):
    class Meta:
        model = IssueStatus
        fields = '__all__'


class IssueStatusSystemSerializer(serializers.ModelSerializer):
    statuses = IssueStatusSerializer(many=True)

    class Meta:
        model = IssueStatusSystem
        fields = '__all__'


class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = File
        fields = '__all__'


class IssueFieldSerializer(serializers.ModelSerializer):
    class Meta:
        model = IssueField
        fields = '__all__'


class EventSerializer(serializers.ModelSerializer):
    file_set = FileSerializer(many=True)

    class Meta:
        model = Event
        fields = '__all__'


class IssueSerializer(serializers.ModelSerializer):
    event_set = EventSerializer(many=True)

    class Meta:
        model = Issue
        fields = '__all__'


class TaskGroupRelationsSerializer(serializers.ModelSerializer):
    class Meta:
        model = TaskGroupRelations
        fields = '__all__'


class TaskSerializer(serializers.ModelSerializer):
    issue_set = IssueSerializer(many=True)
    taskgrouprelations_set = TaskGroupRelationsSerializer(many=True)

    class Meta:
        model = Task
        fields = '__all__'


class DefaultTeacherSerializer(serializers.ModelSerializer):
    class Meta:
        model = DefaultTeacher
        fields = '__all__'


class CourseSerializer(serializers.ModelSerializer):
    defaultteacher_set = DefaultTeacherSerializer(many=True)
    filename_extensions = FilenameExtensionSerializer(many=True)
    groups = GroupSerializer(many=True)
    issue_status_system = IssueStatusSystemSerializer()
    issue_fields = IssueFieldSerializer(many=True)
    mark_system = CourseMarkSystemSerializer()
    task_set = TaskSerializer(many=True)
    teachers = UserSerializer(many=True)
    year = YearSerializer()

    class Meta:
        model = Course
        fields = '__all__'


def serialize(serializer_class, queryset):
    return serializer_class(queryset, many=True, read_only=True).data


# Constants
OUTPUT_PATH = 'shad_data_3.json'
PROFILE_LINKS = [
    'https://anytask.org/accounts/profile/bolyarich',
    'https://anytask.org/accounts/profile/noath',
    'https://anytask.org/accounts/profile/corwin',
    'https://anytask.org/accounts/profile/pew-pew',
    'https://anytask.org/accounts/profile/elevely',
    'https://anytask.org/accounts/profile/rvg77',
    'https://anytask.org/accounts/profile/gostkin',
    'https://anytask.org/accounts/profile/pitamakan',
    'https://anytask.org/accounts/profile/Demidov_Artem',
    'https://anytask.org/accounts/profile/zhog96',
    'https://anytask.org/accounts/profile/zubkovmaxim',
    'https://anytask.org/accounts/profile/kvk1920',
    'https://anytask.org/accounts/profile/alex-kozinov',
    'https://anytask.org/accounts/profile/komendart',
    'https://anytask.org/accounts/profile/tardis.forever',
    'https://anytask.org/accounts/profile/Vemmy124',
    'https://anytask.org/accounts/profile/KMAMONOV',
    'https://anytask.org/accounts/profile/mhuman',
    'https://anytask.org/accounts/profile/AleksIks',
    'https://anytask.org/accounts/profile/lexolordan',
    'https://anytask.org/accounts/profile/cromtus',
    'https://anytask.org/accounts/profile/%2B',
    'https://anytask.org/accounts/profile/SYury',
    'https://anytask.org/accounts/profile/sysak-ma',
    'https://anytask.org/accounts/profile/bulatuseinov',
    'https://anytask.org/accounts/profile/a_chubcheva',
    'https://anytask.org/accounts/profile/yamalutdinovav',
    'https://anytask.org/accounts/profile/Mogbymo'
]
USERNAMES = [link.split('/')[-1] for link in PROFILE_LINKS]
TARGET_COURSE_IDS = []

def dump_users(user_queryset):
    return serialize(UserSerializer, queryset=user_queryset)

def dump_courses(course_queryset):
    return serialize(CourseSerializer, queryset=course_queryset)

def dump():
    # Users
    user_queryset = User.objects.filter(username__in=USERNAMES)
    user_data = dump_users(user_queryset)
    print 'Gathered data for {} user(s)'.format(len(user_data))

    # Courses
    course_queryset = Course.objects.filter(pk__in=TARGET_COURSE_IDS)
    course_data = dump_course(course_queryset)

    print 'Targeted {} course(s)'.format(len(TARGET_COURSE_IDS))
    print 'Gathered data for {} course(s)'.format(len(course_data))

    # Combine to JSON
    renderer = JSONRenderer()
    data = {
        # 'courses': course_data,
        'users': user_data
    }
    with open(OUTPUT_PATH, 'w') as f:
        f.write(renderer.render(data))

    print 'Finished. Output file: {}'.format(OUTPUT_PATH)