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 gettext_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_module_level = models.ForeignKey('books.ModuleLevel', related_name='+', on_delete=models.SET_NULL, null=True) 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_teachers(self): teachers = [] for school_class in self.school_classes.all(): teachers.extend(school_class.get_teachers()) return teachers 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_teachers(self): return list(self.users.filter(user_roles__role__key='teacher')) 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)