class CourseForm(ModelForm):
    schedule_type = forms.ChoiceField(choices=Schedule.TYPES)

    class Meta:
        model = Course
        fields = [
            'subject',
            'active',
            'visible',
            'description',
            'max_participants',
            'archiving',
            'start_time',
            'end_time',
            'schedule_type'
        ]
        help_texts = {
            'subject': _('Choose a subject for your course. '),
            'active':   _(
                        'Choose whether people should be able to join this course '
                        'right now.'
                        ),
            'visible': _('Choose whether people should see the course even if its not active'),
            'description':  _(
                            'A good description is half the battle. '
                            'You can use markdown for formatting.'
                            ),
            'max_participants': _('How many people can join your course. (Can be changed later)'),
            'start_time': _('The course starts on that day.'),
            'end_time': _('The course will end at that day.')
        }
        labels = {
            'max_participants': _('Max nr. of participants'),
            'start_time': _('Begin'),
            'end_time': _('End')
        }

    start_time = forms.DateField(
        widget=forms.DateInput(format='%d.%m.%Y'),
        input_formats=['%d.%m.%Y', '%Y-%m-%d'])
    end_time = forms.DateField(
        widget=forms.DateInput(format=('%d.%m.%Y')),
        input_formats=['%d.%m.%Y', '%Y-%m-%d']) class AddTeacherForm(forms.Form):
    username = forms.CharField(
        help_text=_('Username of the person you want to be a teacher for this course'),
        validators=[username_exists_validator]
    ) class NotifyCourseForm(ContactForm):
    show_sender = forms.BooleanField(
        initial=False,
        required=False,
        help_text=_('Whether to set the email sender to your email address or not.')
    ) class Course(models.Model):
    teacher = models.ManyToManyField(UserInformation, related_name="teacher", blank=True)
    participants = models.ManyToManyField(UserInformation, blank=True, through='Participation')
    active = models.BooleanField(default=False)
    visible = models.BooleanField(default=False)
    subject = models.ForeignKey(subject.Subject, on_delete=models.CASCADE)
    max_participants = models.IntegerField()
    description = models.TextField(blank=True, default="")
    archiving = models.CharField(max_length=1, choices=ARCHIVE_STATUSES)
    start_time = models.DateField(default=django.utils.timezone.now)
    end_time = models.DateField(default=django.utils.timezone.now)

    class IsFull(Exception):
        pass

    class IsInactive(Exception):
        pass

    class IsInvisible(Exception):
        pass

    class IsNotEnrolled(Exception):
        pass

    class IsEnrolled(Exception):
        pass

    class IsArchived(Exception):
        pass


    def __str__(self):
        return self.subject.name

    def enroll(self, student):
        student = get_user_information(student)
        if self._is_participant(student):
            raise self.IsEnrolled
        elif not self.active:
            raise self.IsInactive
        elif self.is_archived():
            raise self.IsArchived
        else:
            if (self.participants.count()):
                ticket_number = Participation.objects.filter(course=self).reverse()[0].ticket_number + 1
            else:
                ticket_number = 1
            Participation.objects.create(participant=student, course=self, ticket_number=ticket_number)

    def unenroll(self, student):

        student = get_user_information(student)

        if self._is_participant(student):
            self.participants.remove(student)
        else:
            raise self.IsNotEnrolled
        # TODO: send notification to first in waiting list

    def _is_participant(self, student):
        student = get_user_information(student)
        return student in self.participants.all()

    def is_participant(self, student):
        return self._is_participant(get_user_information(student)) and not self.is_archived()

    @property
    def saturated(self):
        (curr, max) = self.saturation_level
        return curr >= max

    @property
    def joinable(self):
        """
        Returns whether this course can currently be joined.
        """
        return not self.saturated and self.is_active() and self.is_visible()

    @property
    def saturation_level(self):
        """
        Returns the ratio of currently enrolled participants to maximum allowed
        participants.
        """
        return self.participants.count(), self.max_participants

    @property
    def enrolled_students(self):
        return min(self.participants.count(), self.max_participants)

    @property
    def students_on_queue(self):
        return max(self.participants.count() - self.max_participants, 0)

    def get_description_as_html(self):
        return clean_for_description(markdown(self.description))

    def get_distinct_locations(self):
        return self.schedule.slots.values_list('location', flat=True).distinct()

    def get_locations(self):
        return self.schedule.slots.values_list('location', flat=True)

    def get_queue(self):
        return Participation.objects.filter(course=self)[self.max_participants:]

    def position_in_queue(self, student):
        queue = list(self.get_queue())
        if len(queue) == 0:
            return 0
        users = [p.participant for p in queue]
        user = get_user_information(student)
        if not user in users:
            return 0
        return users.index(user) + 1

    def as_context(self, student=None):
        participants_count, max_participants = self.saturation_level
        sub_name = self.subject.name
        context = {
            'title': sub_name,
            'course_id': self.id,
            'course': self,
            'backurl': reverse('subject', args=[sub_name]),
            'participants_count': participants_count,
            'max_participants': max_participants,
            'course_is_active': self.active,
            'course_is_visible': self.visible,
        }
        if student:
            student = get_user_information(student)
            context['position_in_queue'] = self.position_in_queue(student)
            context['is_subbed'] = student.course_set.filter(id=self.id).exists()

            if self.can_modify(student):
                context['can_modify'] = True
                context['is_teacher'] = self.is_teacher(student)
                context['students'] = self.participants.all()

        return context

    def can_be_archived(self):
        return self.archiving == 't'

    def is_archived(self):
        return self.archiving == 'a'

    def has_description(self):
        return self.description != ''

    def is_teacher(self, student):
        return self.teacher.filter(id=student.id).exists()

    def can_modify(self, student):
        """
        Formerly "is_teacher"; whether or not a user can change details of the course.
        """
        return student.user.has_perm('change_course', self) \
                and student.user.has_perm('delete_course', self)

    def is_active(self):
        return self.active and not self.is_archived()

    def is_visible(self):
        return self.visible and not self.is_archived() class Schedule(models.Model):
    TYPES = [
        ('W', _("weekly")),
        ('O', _('one time')),
    ]

    _type = models.CharField(max_length=1, choices=TYPES)
    course = models.OneToOneField(course.Course, on_delete=models.CASCADE)

    def is_weekly(self):
        """
        Returns whether this course operates on a weekly schedule
        """
        return self._type == 'W'

    def is_one_time(self):
        """
        Returns whether this course happens only a distinct number of times.
        """
        return self._type == 'O'

    @property
    def slots(self):
        if self.is_weekly():
            return WeeklySlot.objects.filter(schedule=self)
        else:
            return DateSlot.objects.filter(schedule=self)

    def get_type(self):
        return self._type

    def set_type(self, type):
        self._type = type

    def __str__(self):
        #return "{type} - {slots}".format(type=self.get__type_display(), slots="; ".join(map(str,self.slots)))
        course = self.course
        return "{} - {} - {}".format(self._type, course.id, course.subject.name) class Subject(models.Model):
    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(blank=True, default="")

    def __str__(self):
        return self.name

    def render_description(self):
        return clean_for_description(markdown(self.description))

    @classmethod
    def get_active(cls):
        """
        Return all subjects which have associated active courses.
        """
        # REVIEW
        # This does not work right now, I have no idea why
        # return cls.objects.annotate(
        #     course_count=models.Count(models.Q(course__active=True))
        # ).filter(course_count__gt=0)
        return filter(
            lambda subject: subject.is_active(),
            cls.objects.all()
        )

    @classmethod
    def get_visible(cls):
        """
        Return al subjects which have associated visible courses.
        """
        return filter(
            lambda subject:subject.is_visible(),
            cls.objects.all()
        )

    @classmethod
    def get_inactive(cls):
        """
        Return all subjects which do not have associated active courses.
        """
        return filter(
            lambda subject: not subject.is_active(),
            cls.objects.all()
        )

    @classmethod
    def get_invisible(cls):
        """
        Return all subjects which do not have associated visible courses.
        """
        return filter(
            lambda subject: not subject.is_visible(),
            cls.objects.all()
        )

    @property
    def active_courses(self):
        """
        Returns all associated courses which are active.
        """
        return self.course_set.filter(active=True, archiving='t')

    def visible_courses(self):
        """
        Returns all associated courses which are visible.
        """
        return self.course_set.filter(visible=True, archiving='t')

    def is_active(self):
        """
        Return true if this subject has any associated courses which are active.
        """
        return self.course_set.filter(active=True, archiving='t').exists()

    def is_visible(self):
        """
        Return true if this subject has any associated courses which are visible.
        """
        return self.course_set.filter(visible=True, archiving='t').exists() def needs_teacher_permissions(func):
    """
    The decorated function must take at least 2 parameters, the request, and a Course object or an id for a course.

    If the current user is registered as a teacher for the course the decorated function is executed, else
    the user is redirected to the login page

    :param func:
    :return:
    """
    @functools.wraps(func)
    @login_required()
    def wrapped(request, course_id, *args, **kwargs):
        try:
            curr_course = Course.objects.get(id=course_id) if not isinstance(course_id, Course) else course_id
        except Course.DoesNotExist:
            return db_error(_('This course does not seem to exist, sorry.'))
        if has_teacher_permissions(request.user.userinformation, curr_course):
            return func(request, course_id, *args, **kwargs)
        else:
            return redirect(reverse('login')+'?next='+request.path)
    return wrapped class UserInformation(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    description = models.TextField(default="")
    public_profile = models.BooleanField(default=False)
    accepted_privacy_policy = models.BooleanField(
        validators=[privacy_policy_consented])

    def __str__(self):
        return '{first} {last}'.format(first=self.user.first_name, last=self.user.last_name)

    def render_description(self):
        return clean_for_user_description(markdown(self.description))

    @staticmethod
    def create(
            username,
            email,
            password,
            first_name,
            last_name,
    ):
        user = User.objects.create_user(
            email=email,
            username=username,
            password=password,
            first_name=first_name,
            last_name=last_name
        )
        user.is_active = False
        user.save()

        return UserInformation.objects.create(user=user, accepted_privacy_policy=True) class ContactForm(forms.Form):
    subject = forms.CharField(help_text=_(
        'This will become the subject field of the resulting email.'))
    content = forms.CharField(
        widget=forms.Textarea,
        help_text=_('This will be the content of the email. HTML is not allowed '
                    'and any html tags will be removed.')
    )

    def clean(self):
        super().clean()
        self.subject = html_clean.clean_all(self.data['subject'])
        self.content = html_clean.clean_all(self.data['content']) DESCR_ALLOWED_TAGS = bleach.ALLOWED_TAGS + ['h2', 'h3', 'h4', 'h5', 'h6', 'br', 'p', 'img']
USER_DESCR_ALLOWED_TAGS = bleach.ALLOWED_TAGS + ['h2', 'h3', 'h4', 'h5', 'h6', 'br', 'p']
def clean_for_user_description(html):
def clean_for_description(html):
def clean_all(html): def db_error(message):
    """
    Report a database error in a view.
    """
    return render_to_response('error/db.html', {'error': message}) def redirect_unless_target(request: HttpRequest, *args, _target_kwd='target', **kwargs):
    """
    Redirects with 'target' get request parameter awareness.
    """
    return redirect(request.GET[_target_kwd]) if _target_kwd in request.GET else redirect(*args, **kwargs)
from django.contrib.auth.decorators import permission_required, login_required
from django.contrib.auth.models import User
from django.urls import reverse
from django.http import HttpRequest
from django.shortcuts import redirect, render_to_response, render
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from guardian.shortcuts import assign_perm
from guardian.shortcuts import remove_perm
from course.forms import CourseForm, AddTeacherForm, NotifyCourseForm
from course.models.course import Course
from course.models.schedule import Schedule
from course.models.subject import Subject
from course.util.permissions import needs_teacher_permissions
from user.models import UserInformation
from user.forms import ContactForm
from util import html_clean
from util.error.reporting import db_error
from util.routing import redirect_unless_target
import itertools
@require_POST
def toggle(request: HttpRequest, course_id: str, active: bool):
    """
    Toggle course status (active/inactive)

    :param request: request information
    :param course_id:
    :param active: active/inactive
    :return:
    """
    # TODO: Delete me
    try:
        curr_course = Course.objects.get(id=course_id)
    except Course.DoesNotExist:
        return db_error(_('Requested course does not exist.'))

    curr_course.active = active
    if not active:
        curr_course.participants.clear()
        curr_course.queue.clear()
    curr_course.save()
    return redirect('course', course_id)


@permission_required('course.add_course')
def create(request):

    if request.method == 'POST':
        form = CourseForm(request.POST)
        if form.is_valid():
            created = form.save(commit=False)
            created.save()
            created.teacher.add(request.user.userinformation)

            Schedule.objects.create(_type=request.POST['schedule_type'], course=created)

            assign_perm(
                'change_course',
                request.user,
                created
            )
            assign_perm(
                'delete_course',
                request.user,
                created
            )

            return redirect('course', created.id)
    else:
        form = CourseForm(initial={
            'description': DEFAULT_COURSE_DESCRIPTION,
            'max_participants': 30,
            'archiving': 't'
        })
        if 'subject' in request.GET:
            subj = int(request.GET['subject'][0])
            if Subject.objects.filter(id=subj).exists():
                form.initial['subject'] = subj

    return render(
        request,
        'course/edit.html',
        {
            'title': _('New Course'),
            'form': form,
            'create': True
        }
    )


@login_required()
@require_POST
@needs_teacher_permissions
def delete(request, course_id):
    try:
        course = Course.objects.get(id=course_id)
    except Course.DoesNotExist:
        return db_error(_('Requested course does not exist.'))

    subj = course.subject.name

    course.delete()
    return redirect('subject', subj)


@needs_teacher_permissions
def add_teacher(request, course_id):
    context = {
        'title': _('Edit Teachers'),
        'course_id': course_id,
        'target': reverse('add-teacher', args=(course_id,))
    }

    try:
        curr_course = Course.objects.get(id=course_id)
    except Course.DoesNotExist:
        return db_error(_('Requested course does not exist.'))

    if request.method == 'POST':
        form = AddTeacherForm(request.POST)
        if form.is_valid():
            try:
                user = User.objects.get(username=form.cleaned_data['username'])

                curr_course.teacher.add(user.userinformation)
                assign_perm(
                    'change_course',
                    user,
                    curr_course
                )
                assign_perm(
                    'delete_course',
                    user,
                    curr_course
                )

                return redirect('add-teacher', course_id)
            except User.DoesNotExist:
                context['error'] = _('The username you entered does not exist.')
    else:

        form = AddTeacherForm()