from django.db import models 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 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 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) 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.DateField(_('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) 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=[ ("TC", _("TC")), ("IMSI", _("IMSI")), ("IMAP", _("IMAP")), ("INFO", _("INFO")), ("GI", _("GI")), ("E", _("E")), ("EE", _("EE")), ("GESC", _("GESC")), ("GMC", _("GMC")), ("MC", _("MC")), ("EDIM", _("EDIM")), ("HUMA", _("Humanities")), ("NA", _("N/A")), ], 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) 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.username def to_dict(self): return self.__dict__ def is_in_group(self, group_name): """If the user is in the group passed in argument (as string)""" if group_name == settings.SITH_GROUPS['public']['name']: return True if group_name == settings.SITH_MAIN_MEMBERS_GROUP: # We check the subscription if asked if 'subscription' in settings.INSTALLED_APPS: from subscription.models import Subscriber s = Subscriber.objects.filter(pk=self.pk).first() if s is not None and s.is_subscribed(): return True else: return False else: return False if group_name[-len(settings.SITH_BOARD_SUFFIX):] == settings.SITH_BOARD_SUFFIX: from club.models import Club name = group_name[:-len(settings.SITH_BOARD_SUFFIX)] c = Club.objects.filter(unix_name=name).first() mem = c.get_membership_for(self) if mem: return mem.role > settings.SITH_MAXIMUM_FREE_ROLE return False if group_name[-len(settings.SITH_MEMBER_SUFFIX):] == settings.SITH_MEMBER_SUFFIX: from club.models import Club name = group_name[:-len(settings.SITH_MEMBER_SUFFIX)] c = Club.objects.filter(unix_name=name).first() mem = c.get_membership_for(self) if mem: return True return False if group_name == settings.SITH_GROUPS['root']['name'] and self.is_superuser: return True return self.groups.filter(name=group_name).exists() @property def is_root(self): return self.is_superuser or self.groups.filter(name=settings.SITH_GROUPS['root']['name']).exists() 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." return self.first_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 """ return timezone.now().year - self.date_of_birth.year def email_user(self, subject, message, from_email=None, **kwargs): """ Sends an email to this User. """ 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_GROUPS['root']['name']): return True return False def can_edit(self, obj): """ Determine if the object can be edited by the user """ if self.is_owner(obj): 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 hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self): return True return False def can_view(self, obj): """ Determine if the object can be viewed by the user """ if self.can_edit(obj): return True if hasattr(obj, "view_groups"): for g in obj.view_groups.all(): if self.is_in_group(g.name): return True if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self): 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.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP) and self.is_subscriber_viewable) or user.is_root def get_mini_item(self): return """