<commit_before># -*- coding: utf-8 -*-
# Copyright (c) 2015 The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details.
"""
    pycroft.model.user
    ~~~~~~~~~~~~~~

    This module contains the class User.

    :copyright: (c) 2011 by AG DSN.
"""
import re

from flask_login import UserMixin
from sqlalchemy import (
    Boolean, BigInteger, CheckConstraint, Column, ForeignKey, Integer,
    String, and_, exists, join, literal, not_, null, or_, select, Sequence,
    Interval, Date, func)
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
from sqlalchemy.orm import backref, object_session, relationship, validates
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.util import has_identity
from sqlalchemy.sql import true, false

from pycroft.helpers.interval import (
    IntervalSet, UnboundedInterval, closed, single)
from pycroft.helpers.user import hash_password, verify_password
from pycroft.model import session, functions
from pycroft.model.base import ModelBase, IntegerIdModel
from pycroft.model.types import DateTimeTz


class IllegalLoginError(ValueError):
    pass


class IllegalEmailError(ValueError):
    pass


class User(IntegerIdModel, UserMixin):
    login = Column(String(40), nullable=False, unique=True)
    name = Column(String(255), nullable=False)
    registered_at = Column(DateTimeTz, nullable=False)
    passwd_hash = Column(String)
    email = Column(String(255), nullable=True)
    birthdate = Column(Date, nullable=True)

    # one to one from User to Account
    account = relationship("Account", backref=backref("user", uselist=False))
    account_id = Column(Integer, ForeignKey("account.id"), nullable=False, index=True)

    unix_account = relationship('UnixAccount')  # backref not really needed.
    unix_account_id = Column(Integer, ForeignKey('unix_account.id'),
                             nullable=True, unique=True)

    # many to one from User to Room
    room_id = Column(Integer, ForeignKey("room.id", ondelete="SET NULL"),
                     nullable=True, index=True)
    room = relationship("Room",
                        backref=backref("users", cascade="all"))

    traffic_groups = relationship("TrafficGroup",
                                  secondary=lambda: Membership.__table__,
                                  viewonly=True)

    property_groups = relationship("PropertyGroup",
                                   secondary=lambda: Membership.__table__,
                                   viewonly=True)

    _current_traffic_balance = relationship(
        'CurrentTrafficBalance',
        primaryjoin='User.id == foreign(CurrentTrafficBalance.user_id)',
        viewonly=True, uselist=False
    )

    @property
    def current_credit(self):
        return self._current_traffic_balance.amount

    #: This is a relationship to the `current_property` view filtering out
    #: the entries with `denied=True`.
    current_properties = relationship(
        'CurrentProperty',
        primaryjoin='and_(User.id == foreign(CurrentProperty.user_id),'
                    '~CurrentProperty.denied)',
        viewonly=True
    )
    #: This is a relationship to the `current_property` view ignoring the
    #: `denied` attribute.
    current_properties_maybe_denied = relationship(
        'CurrentProperty',
        primaryjoin='User.id == foreign(CurrentProperty.user_id)',
        viewonly=True
    )

    login_regex = re.compile(r"""
        ^
        # Must begin with a lowercase character
        [a-z]
        # Can continue with lowercase characters, numbers and some punctuation
        # but between punctuation characters must be characters or numbers
        (?:[.-]?[a-z0-9])+$
        """, re.VERBOSE)
    login_regex_ci = re.compile(login_regex.pattern, re.VERBOSE | re.IGNORECASE)
    email_regex = re.compile(r"^[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+"
                             r"(?:\.[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+)*"
                             r"@(?:[a-zA-Z0-9]+(?:\.|-))+[a-zA-Z]+$")

    blocked_logins = {"abuse", "admin", "administrator", "autoconfig",
                      "broadcasthost", "root", "daemon", "bin", "sys", "sync",
                      "games", "man", "hostmaster", "imap", "info", "is",
                      "isatap", "it", "localdomain", "localhost",
                      "lp", "mail", "mailer-daemon", "news", "uucp", "proxy",
                      # ground control to
                      "majordom", "marketing", "mis", "noc", "website", "api"
                      "noreply", "no-reply", "pop", "pop3", "postmaster",
                      "postgres", "sales", "smtp", "ssladmin", "status",
                      "ssladministrator", "sslwebmaster", "support",
                      "sysadmin", "usenet", "webmaster", "wpad", "www",
                      "wwwadmin", "backup", "msql", "operator", "user",
                      "ftp", "ftpadmin", "guest", "bb", "nobody", "www-data",
                      "bacula", "contact", "email", "privacy", "anonymous",
                      "web", "git", "username", "log", "login", "help", "name"}

    login_character_limit = 22

    def __init__(self, **kwargs):
        password = kwargs.pop('password', None)
        super(User, self).__init__(**kwargs)
        if password is not None:
            self.password = password

    @validates('login')
    def validate_login(self, _, value):
        assert not has_identity(self), "user already in the database - cannot change login anymore!"
        if not self.login_regex.match(value):
            raise IllegalLoginError(
                "Illegal login '{}': Logins must begin with a lower case "
                "letter and may be followed by lower case letters, digits or "
                "punctuation (dash, underscore and dot). Punctuation "
                "characters must be separated by at least on letter or digit."
                .format(value)
            )
        if value in self.blocked_logins:
            raise IllegalLoginError(
                "Illegal login '{}': This login is blocked and may not be used."
                .format(value)
            )
        if len(value) > self.login_character_limit:
            raise IllegalLoginError(
                "Illegal login '{}': Logins are limited to at most {} "
                "characters.".format(value, self.login_character_limit)
            )
        return value

    @validates('email')<commit_msg>Adjust error for wrong username format<commit_after># -*- coding: utf-8 -*-
# Copyright (c) 2015 The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details.
"""
    pycroft.model.user
    ~~~~~~~~~~~~~~

    This module contains the class User.

    :copyright: (c) 2011 by AG DSN.
"""
import re

from flask_login import UserMixin
from sqlalchemy import (
    Boolean, BigInteger, CheckConstraint, Column, ForeignKey, Integer,
    String, and_, exists, join, literal, not_, null, or_, select, Sequence,
    Interval, Date, func)
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
from sqlalchemy.orm import backref, object_session, relationship, validates
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.util import has_identity
from sqlalchemy.sql import true, false

from pycroft.helpers.interval import (
    IntervalSet, UnboundedInterval, closed, single)
from pycroft.helpers.user import hash_password, verify_password
from pycroft.model import session, functions
from pycroft.model.base import ModelBase, IntegerIdModel
from pycroft.model.types import DateTimeTz


class IllegalLoginError(ValueError):
    pass


class IllegalEmailError(ValueError):
    pass


class User(IntegerIdModel, UserMixin):
    login = Column(String(40), nullable=False, unique=True)
    name = Column(String(255), nullable=False)
    registered_at = Column(DateTimeTz, nullable=False)
    passwd_hash = Column(String)
    email = Column(String(255), nullable=True)
    birthdate = Column(Date, nullable=True)

    # one to one from User to Account
    account = relationship("Account", backref=backref("user", uselist=False))
    account_id = Column(Integer, ForeignKey("account.id"), nullable=False, index=True)

    unix_account = relationship('UnixAccount')  # backref not really needed.
    unix_account_id = Column(Integer, ForeignKey('unix_account.id'),
                             nullable=True, unique=True)

    # many to one from User to Room
    room_id = Column(Integer, ForeignKey("room.id", ondelete="SET NULL"),
                     nullable=True, index=True)
    room = relationship("Room",
                        backref=backref("users", cascade="all"))

    traffic_groups = relationship("TrafficGroup",
                                  secondary=lambda: Membership.__table__,
                                  viewonly=True)

    property_groups = relationship("PropertyGroup",
                                   secondary=lambda: Membership.__table__,
                                   viewonly=True)

    _current_traffic_balance = relationship(
        'CurrentTrafficBalance',
        primaryjoin='User.id == foreign(CurrentTrafficBalance.user_id)',
        viewonly=True, uselist=False
    )

    @property
    def current_credit(self):
        return self._current_traffic_balance.amount

    #: This is a relationship to the `current_property` view filtering out
    #: the entries with `denied=True`.
    current_properties = relationship(
        'CurrentProperty',
        primaryjoin='and_(User.id == foreign(CurrentProperty.user_id),'
                    '~CurrentProperty.denied)',
        viewonly=True
    )
    #: This is a relationship to the `current_property` view ignoring the
    #: `denied` attribute.
    current_properties_maybe_denied = relationship(
        'CurrentProperty',
        primaryjoin='User.id == foreign(CurrentProperty.user_id)',
        viewonly=True
    )

    login_regex = re.compile(r"""
        ^
        # Must begin with a lowercase character
        [a-z]
        # Can continue with lowercase characters, numbers and some punctuation
        # but between punctuation characters must be characters or numbers
        (?:[.-]?[a-z0-9])+$
        """, re.VERBOSE)
    login_regex_ci = re.compile(login_regex.pattern, re.VERBOSE | re.IGNORECASE)
    email_regex = re.compile(r"^[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+"
                             r"(?:\.[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+)*"
                             r"@(?:[a-zA-Z0-9]+(?:\.|-))+[a-zA-Z]+$")

    blocked_logins = {"abuse", "admin", "administrator", "autoconfig",
                      "broadcasthost", "root", "daemon", "bin", "sys", "sync",
                      "games", "man", "hostmaster", "imap", "info", "is",
                      "isatap", "it", "localdomain", "localhost",
                      "lp", "mail", "mailer-daemon", "news", "uucp", "proxy",
                      # ground control to
                      "majordom", "marketing", "mis", "noc", "website", "api"
                      "noreply", "no-reply", "pop", "pop3", "postmaster",
                      "postgres", "sales", "smtp", "ssladmin", "status",
                      "ssladministrator", "sslwebmaster", "support",
                      "sysadmin", "usenet", "webmaster", "wpad", "www",
                      "wwwadmin", "backup", "msql", "operator", "user",
                      "ftp", "ftpadmin", "guest", "bb", "nobody", "www-data",
                      "bacula", "contact", "email", "privacy", "anonymous",
                      "web", "git", "username", "log", "login", "help", "name"}

    login_character_limit = 22

    def __init__(self, **kwargs):
        password = kwargs.pop('password', None)
        super(User, self).__init__(**kwargs)
        if password is not None:
            self.password = password

    @validates('login')
    def validate_login(self, _, value):
        assert not has_identity(self), "user already in the database - cannot change login anymore!"
        if not self.login_regex.match(value):
            raise IllegalLoginError(
                "Illegal login '{}': Logins must begin with a lower case "
                "letter and may be followed by lower case letters, digits or "
                "punctuation (dash and dot). Punctuation "
                "characters must be separated by at least on letter or digit."
                .format(value)
            )
        if value in self.blocked_logins:
            raise IllegalLoginError(
                "Illegal login '{}': This login is blocked and may not be used."
                .format(value)
            )
        if len(value) > self.login_character_limit:
            raise IllegalLoginError(
                "Illegal login '{}': Logins are limited to at most {} "
                "characters.".format(value, self.login_character_limit)
            )
        return value

    @validates('email')
{'fraction_non_alphanumeric': 0.12188552188552189, 'fraction_numerical': 0.004489337822671156, 'mean_word_length': 2.498037163046323, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4401276', 'n_tokens_mistral': 3887, 'n_tokens_neox': 3674, 'n_words': 999}