skillbox/server/users/models.py

386 lines
13 KiB
Python

import json
import random
import re
import string
from datetime import date, datetime, timedelta, MINYEAR
from typing import Union
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser, Permission
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware
from django.utils.translation import ugettext_lazy as _
from core.mixins import GraphqlNodeMixin
from users.licenses import get_license_dict, get_default_isbn
from users.managers import LicenseManager, RoleManager, UserManager, UserRoleManager
DEFAULT_SCHOOL_ID = 1
NO_DATE = date(MINYEAR, 1, 1) # date to tag licenses without date
NO_DATETIME = datetime.combine(NO_DATE, datetime.min.time())
AWARE_NO_DATETIME = make_aware(NO_DATETIME)
licenses = get_license_dict()
class User(AbstractUser):
LICENSE_NONE = 'no-license'
LICENSE_VALID = 'valid-license'
LICENSE_EXPIRED = 'expired-license'
last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
recent_modules = models.ManyToManyField('books.Module', related_name='+', through='books.RecentModule')
last_topic = models.ForeignKey('books.Topic', related_name='+', on_delete=models.SET_NULL, null=True)
avatar_url = models.CharField(max_length=254, blank=True, default='')
email = models.EmailField(_('email address'), unique=True)
hep_id = models.PositiveIntegerField(null=True, blank=False)
hep_group_id = models.PositiveIntegerField(null=True, blank=False)
license_expiry_date = models.DateField(blank=False, null=True, default=None)
onboarding_visited = models.BooleanField(default=False)
team = models.ForeignKey('users.Team', on_delete=models.SET_NULL, blank=True, null=True, related_name='members')
# for wagtail autocomplete
autocomplete_search_field = 'username'
def autocomplete_label(self):
return self.username
objects = UserManager()
def get_role_permissions(self):
perms = set()
for role in Role.objects.get_roles_for_user(self):
perms = perms.union(
('{}.{}'.format(r.content_type.app_label, r.codename) for r in role.role_permission.all())
)
return perms
def get_all_permissions(self, obj=None):
"""
works as long as we have only a single school
:param obj:
:return: django permissions and school permissions for single default school
"""
django_permissions = super().get_all_permissions(obj)
return django_permissions.union(self.get_role_permissions())
def has_perm(self, perm, obj=None):
return super(User, self).has_perm(perm, obj) or perm in self.get_all_permissions(obj)
def users_in_same_school_class(self):
return User.objects.filter(school_classes__users=self.pk)
def users_in_active_school_class(self):
return self.selected_class.users.all() if self.selected_class is not None else []
def get_teacher(self):
if self.is_teacher():
return self
elif self.school_classes.count() > 0:
return self.school_classes.first().get_teacher()
else:
return None
def is_teacher(self):
return self.user_roles.filter(role__key='teacher').exists()
@cached_property
def selected_class(self) -> Union['SchoolClass', None]:
return self._get_selected_class()
def _get_selected_class(self) -> Union['SchoolClass', None]:
try:
settings = UserSetting.objects.get(user=self)
return settings.selected_class
except ObjectDoesNotExist:
if self.school_classes.count() > 0:
default_selected_class = self.school_classes.first()
UserSetting.objects.create(selected_class=default_selected_class, user=self)
return default_selected_class
else:
return None
def sync_with_hep_data(self, hep_data):
data_has_changed = False
if self.email != hep_data['email']:
self.email = hep_data['email']
self.username = hep_data['email']
data_has_changed = True
if self.first_name != hep_data['first_name']:
self.first_name = hep_data['first_name']
data_has_changed = True
if self.last_name != hep_data['last_name']:
self.last_name = hep_data['last_name']
data_has_changed = True
if data_has_changed:
self.save()
def set_selected_class(self, school_class):
user_settings, created = UserSetting.objects.get_or_create(user=self)
user_settings.selected_class = school_class
user_settings.save()
@property
def can_manage_school_class_content(self):
return self.has_perm('users.can_manage_school_class_content')
@property
def full_name(self):
return self.get_full_name()
@property
def read_only(self):
return self.get_license_status() == User.LICENSE_EXPIRED
def get_license_status(self):
if self.license_expiry_date is None:
return User.LICENSE_NONE
if self.license_expiry_date >= date.today():
return User.LICENSE_VALID
return User.LICENSE_EXPIRED
class Meta:
ordering = ['pk', ]
class GroupWithCode(models.Model):
class Meta:
abstract = True
name = models.CharField(max_length=100, blank=False, null=False, unique=True)
is_deleted = models.BooleanField(blank=False, null=False, default=False)
code = models.CharField('Code zum Beitreten', blank=True, null=True, max_length=10, unique=True, default=None)
def generate_code(self):
letters = string.ascii_lowercase
digits = string.digits
code = ''.join(random.choice(letters) for i in range(4)) + ''.join(random.choice(digits) for i in range(2))
try:
self.__class__.objects.get(code=code)
self.generate_code()
except self.__class__.DoesNotExist:
self.code = code.upper()
self.save()
class Team(GroupWithCode):
creator = models.ForeignKey(get_user_model(), null=True, on_delete=models.SET_NULL, blank=True, related_name='+')
class Meta:
verbose_name = 'Team'
verbose_name_plural = 'Teams'
def __str__(self):
return self.name
class SchoolClass(GroupWithCode, GraphqlNodeMixin):
users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True,
through='users.SchoolClassMember')
class Meta:
verbose_name = 'Schulklasse'
verbose_name_plural = 'Schulklassen'
def __str__(self):
return '{}'.format(self.name)
@classmethod
def generate_default_group_name(cls, user=None):
prefix = 'Meine Klasse'
if user is not None:
return f'{prefix} {user.pk}'
prefix_regex = r'Meine Klasse (\d+)'
initial_default_group = '{} 1'.format(prefix)
my_group_filter = cls.objects.filter(name__startswith=prefix).order_by('-pk')
if len(my_group_filter) == 0:
return initial_default_group
match = re.search(prefix_regex, my_group_filter[0].name)
if not match:
return initial_default_group
index = int(match.group(1))
return '{} {}'.format(prefix, index + 1)
@classmethod
def create_default_group_for_teacher(cls, user):
default_class_name = cls.generate_default_group_name(user)
default_class = cls.objects.create(name=default_class_name)
SchoolClassMember.objects.create(
user=user,
school_class=default_class
)
def is_user_in_schoolclass(self, user):
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0
def get_teacher(self):
return self.users.filter(user_roles__role__key='teacher').first()
def save(self, *args, **kwargs):
if self.code == '': # '' can't be unique, so we null it
self.code = None
super().save(*args, **kwargs)
class Role(models.Model):
key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True)
name = models.CharField(_('Name'), max_length=100, blank=False, null=False)
role_permission = models.ManyToManyField(Permission, verbose_name=_('Role permission'),
blank=True, related_name="role_set", related_query_name="role")
objects = RoleManager()
def __str__(self):
return self.name
def permissions_as_code(self):
return self.role_permission.values_list('codename', flat=True)
def has_permission(self, permission_name):
try:
self.role_permission.get(codename=permission_name)
return True
except Permission.DoesNotExist:
return False
@staticmethod
def create_key(name):
return name.lower()
class Meta:
permissions = (
# ("can_edit_events", "Can edit events"),
# ("can_edit_own_comments", "Can edit own comments"),
# ("can_delete_comments", "Can delete comments"),
('can_manage_school_class_content', 'Can manage contents for assigned school clases'),
# ("can_admin_school", "Can admin school"),
)
class UserRole(models.Model):
user = models.ForeignKey(User, blank=False, null=False, on_delete=models.CASCADE, related_name='user_roles')
role = models.ForeignKey(Role, blank=False, null=False, on_delete=models.CASCADE, related_name='user_roles')
objects = UserRoleManager()
@property
def groups(self):
return SchoolClass.objects.filter(users=self.user.id)
@property
def group_ids(self):
return map(lambda g: g.id, self.groups)
@classmethod
def get_roles_for_user(cls, user):
roles = UserRole.objects.filter(user=user)
return roles
@classmethod
def get_role_for_user(cls, user):
return UserRole.get_roles_for_user(user).first()
def __str__(self):
return '%s: %s' % (self.role, self.user)
class UserSetting(models.Model):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting')
selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE)
class UserData(models.Model):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_data')
accepted_terms = models.BooleanField(default=False)
class License(models.Model):
for_role = models.ForeignKey(Role, blank=False, null=True, on_delete=models.CASCADE)
licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE)
expire_date = models.DateField(blank=False, null=True, )
order_id = models.IntegerField(blank=False, null=False, default=-1)
raw = models.TextField(default='')
isbn = models.CharField(max_length=50, blank=False, null=False,
default=get_default_isbn) # student license
created_at = models.DateTimeField(auto_now_add=True)
hep_created_at = models.DateTimeField(default=AWARE_NO_DATETIME)
new_api_raw = models.TextField(default='')
objects = LicenseManager()
def is_teacher_license(self):
return self.for_role.key == RoleManager.TEACHER_KEY
def is_valid(self):
date = make_aware(datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day))
return License.is_product_active(date, self.isbn)
def _read_as_json(self, raw_string: str) -> dict:
return json.loads(raw_string.replace("'", '"').replace('True', 'true').replace('False', 'false')
.replace('None', 'null'))
def get_hep_start_date(self) -> date:
hep_data = self._read_as_json(self.raw)
if 'updated_at' in hep_data: # old key from Magento. Format: 2020-06-22 18:45:13
date_string = hep_data['created_at']
return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S').date()
return NO_DATE
def get_hep_isbn(self) -> str:
hep_data = self._read_as_json(self.raw)
if self._is_old_api(hep_data):
return hep_data['sku']
elif self._is_new_api(hep_data):
return hep_data['isbn']
else:
return ''
def is_new_api(self) -> bool:
hep_data = self._read_as_json(self.raw)
return self._is_new_api(hep_data)
def is_old_api(self) -> bool:
hep_data = self._read_as_json(self.raw)
return self._is_old_api(hep_data)
def _is_new_api(self, hep_data: dict) -> bool:
return 'isbn' in hep_data
def _is_old_api(self, hep_data: dict) -> bool:
return 'sku' in hep_data
@staticmethod
def is_product_active(expiry_date, isbn):
now = timezone.now()
if not is_aware(expiry_date):
expiry_date = make_aware(expiry_date)
try:
return expiry_date >= now >= expiry_date - timedelta(days=licenses[isbn]['duration'])
except KeyError: # this can happen if the platform changed, e.g. during local testing
return False
def __str__(self):
return f'License for role: {self.for_role}'
class SchoolClassMember(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
school_class = models.ForeignKey(SchoolClass, on_delete=models.CASCADE)
active = models.BooleanField(default=True)