362 lines
12 KiB
Python
362 lines
12 KiB
Python
import smtplib
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.hashers import make_password
|
|
from django.contrib.auth.models import (
|
|
AbstractBaseUser,
|
|
BaseUserManager,
|
|
Group as DjangoGroup,
|
|
GroupManager as _GroupManager,
|
|
Permission,
|
|
PermissionsMixin,
|
|
)
|
|
from django.core import mail
|
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
from django.db import models
|
|
from django.db.models import Prefetch
|
|
from django.utils import timezone
|
|
from jsonfield import JSONField
|
|
|
|
from ..core.config import config
|
|
from ..utils.auth import GROUP_ADMIN_PK
|
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, RESTModelMixin
|
|
from .access_permissions import (
|
|
GroupAccessPermissions,
|
|
PersonalNoteAccessPermissions,
|
|
UserAccessPermissions,
|
|
)
|
|
|
|
|
|
class UserManager(BaseUserManager):
|
|
"""
|
|
Customized manager that creates new users only with a password and a
|
|
username. It also supports our get_full_queryset method.
|
|
"""
|
|
|
|
def get_full_queryset(self):
|
|
"""
|
|
Returns the normal queryset with all users. In the background all
|
|
groups are prefetched from the database together with all permissions
|
|
and content types.
|
|
"""
|
|
return self.get_queryset().prefetch_related(
|
|
Prefetch(
|
|
"groups",
|
|
queryset=Group.objects.select_related("group_ptr").prefetch_related(
|
|
Prefetch(
|
|
"permissions",
|
|
queryset=Permission.objects.select_related("content_type"),
|
|
)
|
|
),
|
|
)
|
|
)
|
|
|
|
def create_user(self, username, password, skip_autoupdate=False, **kwargs):
|
|
"""
|
|
Creates a new user only with a password and a username.
|
|
"""
|
|
user = self.model(username=username, **kwargs)
|
|
user.set_password(password)
|
|
user.save(skip_autoupdate=skip_autoupdate, using=self._db)
|
|
return user
|
|
|
|
def create_or_reset_admin_user(self, skip_autoupdate=False):
|
|
"""
|
|
Creates an user with the username 'admin'. If such a user already
|
|
exists, resets it. The password is (re)set to 'admin'. The user
|
|
becomes member of the group 'Admin'.
|
|
"""
|
|
created = False
|
|
try:
|
|
admin = self.get(username="admin")
|
|
except ObjectDoesNotExist:
|
|
admin = self.model(username="admin", last_name="Administrator")
|
|
created = True
|
|
admin.default_password = "admin"
|
|
admin.password = make_password(admin.default_password)
|
|
admin.save(skip_autoupdate=skip_autoupdate)
|
|
admin.groups.add(GROUP_ADMIN_PK)
|
|
return created
|
|
|
|
def generate_username(self, first_name, last_name):
|
|
"""
|
|
Generates a username from first name and last name.
|
|
"""
|
|
first_name = first_name.strip()
|
|
last_name = last_name.strip()
|
|
|
|
if first_name and last_name:
|
|
base_name = " ".join((first_name, last_name))
|
|
else:
|
|
base_name = first_name or last_name
|
|
if not base_name:
|
|
raise ValueError(
|
|
"Either 'first_name' or 'last_name' must not be " "empty."
|
|
)
|
|
|
|
if not self.filter(username=base_name).exists():
|
|
generated_username = base_name
|
|
else:
|
|
counter = 0
|
|
while True:
|
|
counter += 1
|
|
test_name = f"{base_name} {counter}"
|
|
if not self.filter(username=test_name).exists():
|
|
generated_username = test_name
|
|
break
|
|
|
|
return generated_username
|
|
|
|
|
|
class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
|
"""
|
|
Model for users in OpenSlides. A client can login as an user with
|
|
credentials. An user can also just be used as representation for a person
|
|
in other OpenSlides apps like motion submitter or (assignment) election
|
|
candidates.
|
|
"""
|
|
|
|
access_permissions = UserAccessPermissions()
|
|
|
|
USERNAME_FIELD = "username"
|
|
|
|
username = models.CharField(max_length=255, unique=True, blank=True)
|
|
|
|
auth_type = models.CharField(max_length=64, default="default")
|
|
|
|
first_name = models.CharField(max_length=255, blank=True)
|
|
|
|
last_name = models.CharField(max_length=255, blank=True)
|
|
|
|
gender = models.CharField(max_length=255, blank=True)
|
|
|
|
email = models.EmailField(blank=True)
|
|
|
|
last_email_send = models.DateTimeField(blank=True, null=True)
|
|
|
|
# TODO: Try to remove the default argument in the following fields.
|
|
|
|
structure_level = models.CharField(max_length=255, blank=True, default="")
|
|
|
|
title = models.CharField(max_length=50, blank=True, default="")
|
|
|
|
number = models.CharField(max_length=50, blank=True, default="")
|
|
|
|
about_me = models.TextField(blank=True, default="")
|
|
|
|
comment = models.TextField(blank=True, default="")
|
|
|
|
default_password = models.CharField(max_length=100, blank=True, default="")
|
|
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
is_present = models.BooleanField(default=False)
|
|
|
|
is_committee = models.BooleanField(default=False)
|
|
|
|
objects = UserManager()
|
|
|
|
class Meta:
|
|
default_permissions = ()
|
|
permissions = (
|
|
("can_see_name", "Can see names of users"),
|
|
(
|
|
"can_see_extra_data",
|
|
"Can see extra data of users (e.g. present and comment)",
|
|
),
|
|
("can_change_password", "Can change its own password"),
|
|
("can_manage", "Can manage users"),
|
|
)
|
|
ordering = ("last_name", "first_name", "username")
|
|
|
|
def __str__(self):
|
|
# Strip white spaces from the name parts
|
|
first_name = self.first_name.strip()
|
|
last_name = self.last_name.strip()
|
|
|
|
# The user has a last_name and a first_name
|
|
if first_name and last_name:
|
|
name = " ".join((self.first_name, self.last_name))
|
|
# The user has only a first_name or a last_name or no name
|
|
else:
|
|
name = first_name or last_name or self.username
|
|
|
|
# Return result
|
|
return name
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Overridden method to skip autoupdate if only last_login field was
|
|
updated as it is done during login.
|
|
"""
|
|
if kwargs.get("update_fields") == ["last_login"]:
|
|
kwargs["skip_autoupdate"] = True
|
|
return super().save(*args, **kwargs)
|
|
|
|
def has_perm(self, perm):
|
|
"""
|
|
This method is closed. Do not use it but use openslides.utils.auth.has_perm.
|
|
"""
|
|
raise RuntimeError(
|
|
"Do not use user.has_perm() but use openslides.utils.auth.has_perm"
|
|
)
|
|
|
|
def send_invitation_email(
|
|
self, connection, subject, message, skip_autoupdate=False
|
|
):
|
|
"""
|
|
Sends an invitation email to the users. Returns True on success, False on failiure.
|
|
May raise an ValidationError, if something went wrong.
|
|
"""
|
|
if not self.email:
|
|
return False
|
|
|
|
# Custom dict class that for formatstrings with entries like {not_existent}
|
|
# no error is raised and this is replaced with ''.
|
|
class format_dict(dict):
|
|
def __missing__(self, key):
|
|
return ""
|
|
|
|
message_format = format_dict(
|
|
{
|
|
"name": str(self),
|
|
"event_name": config["general_event_name"],
|
|
"url": config["users_pdf_url"],
|
|
"username": self.username,
|
|
"password": self.default_password,
|
|
}
|
|
)
|
|
try:
|
|
message = message.format(**message_format)
|
|
except KeyError as err:
|
|
raise ValidationError({"detail": "Invalid property {0}", "args": [err]})
|
|
|
|
subject_format = format_dict(
|
|
{"event_name": config["general_event_name"], "username": self.username}
|
|
)
|
|
try:
|
|
subject = subject.format(**subject_format)
|
|
except KeyError as err:
|
|
raise ValidationError({"detail": "Invalid property {0}", "args": [err]})
|
|
|
|
# Create an email and send it.
|
|
email = mail.EmailMessage(
|
|
subject,
|
|
message,
|
|
config["users_email_sender"] + " <" + settings.DEFAULT_FROM_EMAIL + ">",
|
|
[self.email],
|
|
reply_to=[config["users_email_replyto"]],
|
|
)
|
|
try:
|
|
count = connection.send_messages([email])
|
|
except smtplib.SMTPDataError as e:
|
|
error = e.smtp_code
|
|
helptext = ""
|
|
if error == 554: # The server does not accept our connection. The code is
|
|
# something like "transaction failed" or "No SMTP service here"
|
|
helptext = " Is the email sender correct?"
|
|
connection.close()
|
|
raise ValidationError(
|
|
{
|
|
"detail": "Error {0}. Cannot send email.{1}",
|
|
"args": [error, helptext],
|
|
}
|
|
)
|
|
except smtplib.SMTPRecipientsRefused:
|
|
pass # Run into returning false later
|
|
except smtplib.SMTPAuthenticationError as e:
|
|
# Nice error message on auth failure
|
|
raise ValidationError(
|
|
{
|
|
"detail": "Error {0}: Authentication failure. Please contact your local administrator.",
|
|
"args": [e.smtp_code],
|
|
}
|
|
)
|
|
else:
|
|
if count == 1:
|
|
self.email_send = True
|
|
self.last_email_send = timezone.now()
|
|
self.save(skip_autoupdate=skip_autoupdate)
|
|
return True
|
|
|
|
return False
|
|
|
|
@property
|
|
def session_auth_hash(self):
|
|
"""
|
|
Returns the session auth hash of a user as attribute.
|
|
|
|
Needed for the django rest framework.
|
|
"""
|
|
return self.get_session_auth_hash()
|
|
|
|
|
|
class GroupManager(_GroupManager):
|
|
"""
|
|
Customized manager that supports our get_full_queryset method.
|
|
"""
|
|
|
|
def get_full_queryset(self):
|
|
"""
|
|
Returns the normal queryset with all groups. In the background all
|
|
permissions with the content types are prefetched from the database.
|
|
"""
|
|
return (
|
|
self.get_queryset()
|
|
.select_related("group_ptr")
|
|
.prefetch_related(
|
|
Prefetch(
|
|
"permissions",
|
|
queryset=Permission.objects.select_related("content_type"),
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
class Group(RESTModelMixin, DjangoGroup):
|
|
"""
|
|
Extend the django group with support of our REST and caching system.
|
|
"""
|
|
|
|
access_permissions = GroupAccessPermissions()
|
|
objects = GroupManager()
|
|
|
|
class Meta:
|
|
default_permissions = ()
|
|
|
|
|
|
class PersonalNoteManager(models.Manager):
|
|
"""
|
|
Customized model manager to support our get_full_queryset method.
|
|
"""
|
|
|
|
def get_full_queryset(self):
|
|
"""
|
|
Returns the normal queryset with all personal notes. In the background all
|
|
users are prefetched from the database.
|
|
"""
|
|
return self.get_queryset().select_related("user")
|
|
|
|
|
|
class PersonalNote(RESTModelMixin, models.Model):
|
|
"""
|
|
Model for personal notes (e. g. likes/stars) of a user concerning different
|
|
openslides objects like motions.
|
|
"""
|
|
|
|
access_permissions = PersonalNoteAccessPermissions()
|
|
|
|
personalized_model = True
|
|
"""
|
|
Each model belongs to one user. This relation is set during creation and
|
|
will not be changed.
|
|
"""
|
|
|
|
objects = PersonalNoteManager()
|
|
|
|
user = models.OneToOneField(User, on_delete=CASCADE_AND_AUTOUPDATE)
|
|
notes = JSONField()
|
|
|
|
class Meta:
|
|
default_permissions = ()
|