# -*- coding:utf-8 -* # # Copyright 2016,2017,2018 # - Skia # - Sli # # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # http://ae.utbm.fr. # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License a published by the Free Software # Foundation; either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA. # # import importlib from django.db import models from django.core.mail import send_mail from django.contrib.auth.models import ( AbstractBaseUser, PermissionsMixin, UserManager, Group as AuthGroup, GroupManager as AuthGroupManager, AnonymousUser as AuthAnonymousUser, ) from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.core import validators from django.core.exceptions import ValidationError, PermissionDenied from django.core.urlresolvers import reverse from django.conf import settings from django.db import transaction from django.contrib.staticfiles.storage import staticfiles_storage from django.utils.html import escape from django.utils.functional import cached_property import os from phonenumber_field.modelfields import PhoneNumberField from datetime import datetime, timedelta, date import unicodedata class RealGroupManager(AuthGroupManager): def get_queryset(self): return super(RealGroupManager, self).get_queryset().filter(is_meta=False) class MetaGroupManager(AuthGroupManager): def get_queryset(self): return super(MetaGroupManager, self).get_queryset().filter(is_meta=True) class Group(AuthGroup): is_meta = models.BooleanField( _("meta group status"), default=False, help_text=_("Whether a group is a meta group or not"), ) description = models.CharField(_("description"), max_length=60) class Meta: ordering = ["name"] def get_absolute_url(self): """ This is needed for black magic powered UpdateView's children """ return reverse("core:group_list") class MetaGroup(Group): objects = MetaGroupManager() class Meta: proxy = True def __init__(self, *args, **kwargs): super(MetaGroup, self).__init__(*args, **kwargs) self.is_meta = True class RealGroup(Group): objects = RealGroupManager() class Meta: proxy = True def validate_promo(value): start_year = settings.SITH_SCHOOL_START_YEAR delta = (date.today() + timedelta(days=180)).year - start_year if value < 0 or delta < value: raise ValidationError( _("%(value)s is not a valid promo (between 0 and %(end)s)"), params={"value": value, "end": delta}, ) class User(AbstractBaseUser): """ Defines the base user class, useable in every app This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()). Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth """ username = models.CharField( _("username"), max_length=254, unique=True, help_text=_( "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only." ), validators=[ validators.RegexValidator( r"^[\w.+-]+$", _( "Enter a valid username. This value may contain only " "letters, numbers " "and ./+/-/_ characters." ), ) ], error_messages={"unique": _("A user with that username already exists.")}, ) first_name = models.CharField(_("first name"), max_length=64) last_name = models.CharField(_("last name"), max_length=64) email = models.EmailField(_("email address"), unique=True) date_of_birth = models.DateField(_("date of birth"), blank=True, null=True) nick_name = models.CharField(_("nick name"), max_length=64, null=True, blank=True) is_staff = models.BooleanField( _("staff status"), default=False, help_text=_("Designates whether the user can log into this admin site."), ) is_active = models.BooleanField( _("active"), default=True, help_text=_( "Designates whether this user should be treated as active. " "Unselect this instead of deleting accounts." ), ) date_joined = models.DateField(_("date joined"), auto_now_add=True) last_update = models.DateTimeField(_("last update"), auto_now=True) is_superuser = models.BooleanField( _("superuser"), default=False, help_text=_("Designates whether this user is a superuser. "), ) groups = models.ManyToManyField(RealGroup, related_name="users", blank=True) home = models.OneToOneField( "SithFile", related_name="home_of", verbose_name=_("home"), null=True, blank=True, on_delete=models.SET_NULL, ) profile_pict = models.OneToOneField( "SithFile", related_name="profile_of", verbose_name=_("profile"), null=True, blank=True, on_delete=models.SET_NULL, ) avatar_pict = models.OneToOneField( "SithFile", related_name="avatar_of", verbose_name=_("avatar"), null=True, blank=True, on_delete=models.SET_NULL, ) scrub_pict = models.OneToOneField( "SithFile", related_name="scrub_of", verbose_name=_("scrub"), null=True, blank=True, on_delete=models.SET_NULL, ) sex = models.CharField( _("sex"), max_length=10, choices=[("MAN", _("Man")), ("WOMAN", _("Woman"))], default="MAN", ) tshirt_size = models.CharField( _("tshirt size"), max_length=5, choices=[ ("-", _("-")), ("XS", _("XS")), ("S", _("S")), ("M", _("M")), ("L", _("L")), ("XL", _("XL")), ("XXL", _("XXL")), ("XXXL", _("XXXL")), ], default="-", ) role = models.CharField( _("role"), max_length=15, choices=[ ("STUDENT", _("Student")), ("ADMINISTRATIVE", _("Administrative agent")), ("TEACHER", _("Teacher")), ("AGENT", _("Agent")), ("DOCTOR", _("Doctor")), ("FORMER STUDENT", _("Former student")), ("SERVICE", _("Service")), ], blank=True, default="", ) department = models.CharField( _("department"), max_length=15, choices=settings.SITH_PROFILE_DEPARTMENTS, default="NA", blank=True, ) dpt_option = models.CharField( _("dpt option"), max_length=32, blank=True, default="" ) semester = models.CharField(_("semester"), max_length=5, blank=True, default="") quote = models.CharField(_("quote"), max_length=256, blank=True, default="") school = models.CharField(_("school"), max_length=80, blank=True, default="") promo = models.IntegerField( _("promo"), validators=[validate_promo], null=True, blank=True ) forum_signature = models.TextField( _("forum signature"), max_length=256, blank=True, default="" ) second_email = models.EmailField(_("second email address"), null=True, blank=True) phone = PhoneNumberField(_("phone"), null=True, blank=True) parent_phone = PhoneNumberField(_("parent phone"), null=True, blank=True) address = models.CharField(_("address"), max_length=128, blank=True, default="") parent_address = models.CharField( _("parent address"), max_length=128, blank=True, default="" ) is_subscriber_viewable = models.BooleanField( _("is subscriber viewable"), default=True ) godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True) objects = UserManager() USERNAME_FIELD = "username" # REQUIRED_FIELDS = ['email'] def has_module_perms(self, package_name): return self.is_active def has_perm(self, perm, obj=None): return self.is_active and self.is_superuser def get_absolute_url(self): """ This is needed for black magic powered UpdateView's children """ return reverse("core:user_profile", kwargs={"user_id": self.pk}) def __str__(self): return self.get_display_name() def to_dict(self): return self.__dict__ @cached_property def was_subscribed(self): return self.subscriptions.exists() @cached_property def is_subscribed(self): s = self.subscriptions.filter( subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now() ) return s.exists() _club_memberships = {} _group_names = {} _group_ids = {} def is_in_group(self, group_name): """If the user is in the group passed in argument (as string or by id)""" group_id = 0 g = None if isinstance(group_name, int): # Handle the case where group_name is an ID if group_name in User._group_ids.keys(): g = User._group_ids[group_name] else: g = Group.objects.filter(id=group_name).first() User._group_ids[group_name] = g else: if group_name in User._group_names.keys(): g = User._group_names[group_name] else: g = Group.objects.filter(name=group_name).first() User._group_names[group_name] = g if g: group_name = g.name group_id = g.id else: return False if group_id == settings.SITH_GROUP_PUBLIC_ID: return True if group_id == settings.SITH_GROUP_SUBSCRIBERS_ID: return self.is_subscribed if group_id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID: return self.was_subscribed if ( group_name == settings.SITH_MAIN_MEMBERS_GROUP ): # We check the subscription if asked return self.is_subscribed if group_name[-len(settings.SITH_BOARD_SUFFIX) :] == settings.SITH_BOARD_SUFFIX: name = group_name[: -len(settings.SITH_BOARD_SUFFIX)] if name in User._club_memberships.keys(): mem = User._club_memberships[name] else: from club.models import Club c = Club.objects.filter(unix_name=name).first() mem = c.get_membership_for(self) User._club_memberships[name] = mem if mem: return mem.role > settings.SITH_MAXIMUM_FREE_ROLE return False if ( group_name[-len(settings.SITH_MEMBER_SUFFIX) :] == settings.SITH_MEMBER_SUFFIX ): name = group_name[: -len(settings.SITH_MEMBER_SUFFIX)] if name in User._club_memberships.keys(): mem = User._club_memberships[name] else: from club.models import Club c = Club.objects.filter(unix_name=name).first() mem = c.get_membership_for(self) User._club_memberships[name] = mem if mem: return True return False if group_id == settings.SITH_GROUP_ROOT_ID and self.is_superuser: return True return group_name in self.cached_groups_names @cached_property def cached_groups_names(self): return [g.name for g in self.groups.all()] @cached_property def is_root(self): return ( self.is_superuser or self.groups.filter(id=settings.SITH_GROUP_ROOT_ID).exists() ) @cached_property def is_board_member(self): from club.models import Club return ( Club.objects.filter(unix_name=settings.SITH_MAIN_CLUB["unix_name"]) .first() .has_rights_in_club(self) ) @cached_property def can_create_subscription(self): from club.models import Club for club in Club.objects.filter( id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS ).all(): if club.has_rights_in_club(self): return True return False @cached_property def is_launderette_manager(self): from club.models import Club return ( Club.objects.filter( unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ) .first() .get_membership_for(self) ) @cached_property def is_banned_alcohol(self): return self.is_in_group(settings.SITH_GROUP_BANNED_ALCOHOL_ID) @cached_property def is_banned_counter(self): return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID) def save(self, *args, **kwargs): create = False with transaction.atomic(): if self.id: old = User.objects.filter(id=self.id).first() if old and old.username != self.username: self._change_username(self.username) else: create = True super(User, self).save(*args, **kwargs) if ( create and settings.IS_OLD_MYSQL_PRESENT ): # Create user on the old site: TODO remove me! import MySQLdb try: db = MySQLdb.connect(**settings.OLD_MYSQL_INFOS) c = db.cursor() c.execute( """INSERT INTO utilisateurs (id_utilisateur, nom_utl, prenom_utl, email_utl, hash_utl, ae_utl) VALUES (%s, %s, %s, %s, %s, %s)""", ( self.id, self.last_name, self.first_name, self.email, "valid", "0", ), ) db.commit() except Exception as e: with open(settings.BASE_DIR + "/user_fail.log", "a") as f: print( "FAIL to add user %s (%s %s - %s) to old site" % (self.id, self.first_name, self.last_name, self.email), file=f, ) print("Reason: %s" % (repr(e)), file=f) db.rollback() def make_home(self): if self.home is None: home_root = SithFile.objects.filter(parent=None, name="users").first() if home_root is not None: home = SithFile(parent=home_root, name=self.username, owner=self) home.save() self.home = home self.save() def _change_username(self, new_name): u = User.objects.filter(username=new_name).first() if u is None: if self.home: self.home.name = new_name self.home.save() else: raise ValidationError(_("A user with that username already exists")) def get_profile(self): return { "last_name": self.last_name, "first_name": self.first_name, "nick_name": self.nick_name, "date_of_birth": self.date_of_birth, } def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. """ full_name = "%s %s" % (self.first_name, self.last_name) return full_name.strip() def get_short_name(self): "Returns the short name for the user." if self.nick_name: return self.nick_name return self.first_name + " " + self.last_name def get_display_name(self): """ Returns the display name of the user. A nickname if possible, otherwise, the full name """ if self.nick_name: return "%s (%s)" % (self.get_full_name(), self.nick_name) return self.get_full_name() def get_age(self): """ Returns the age """ today = timezone.now() born = self.date_of_birth return ( today.year - born.year - ((today.month, today.day) < (born.month, born.day)) ) def email_user(self, subject, message, from_email=None, **kwargs): """ Sends an email to this User. """ if from_email is None: from_email = settings.DEFAULT_FROM_EMAIL send_mail(subject, message, from_email, [self.email], **kwargs) def generate_username(self): """ Generates a unique username based on the first and last names. For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists Returns the generated username """ def remove_accents(data): return "".join( x for x in unicodedata.normalize("NFKD", data) if unicodedata.category(x)[0] == "L" ).lower() user_name = ( remove_accents(self.first_name[0] + self.last_name) .encode("ascii", "ignore") .decode("utf-8") ) un_set = [u.username for u in User.objects.all()] if user_name in un_set: i = 1 while user_name + str(i) in un_set: i += 1 user_name += str(i) self.username = user_name return user_name def is_owner(self, obj): """ Determine if the object is owned by the user """ if hasattr(obj, "is_owned_by") and obj.is_owned_by(self): return True if hasattr(obj, "owner_group") and self.is_in_group(obj.owner_group.name): return True if self.is_superuser or self.is_in_group(settings.SITH_GROUP_ROOT_ID): return True return False def can_edit(self, obj): """ Determine if the object can be edited by the user """ if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self): return True if hasattr(obj, "edit_groups"): for g in obj.edit_groups.all(): if self.is_in_group(g.name): return True if isinstance(obj, User) and obj == self: return True if self.is_owner(obj): return True return False def can_view(self, obj): """ Determine if the object can be viewed by the user """ if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self): return True if hasattr(obj, "view_groups"): for g in obj.view_groups.all(): if self.is_in_group(g.name): return True if self.can_edit(obj): return True return False def can_be_edited_by(self, user): return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root def can_be_viewed_by(self, user): return (user.was_subscribed and self.is_subscriber_viewable) or user.is_root def get_mini_item(self): return """