def get_user_from_identifier(settings, identifier): """Get an actual User object from an identifier""" providers = list(auth.strip() for auth in settings.get('authenticators').split(',')) identities = Identity.find_all(Identity.provider.in_(providers), Identity.identifier == identifier) if identities: return sorted(identities, key=lambda x: providers.index(x.provider))[0].user for provider in providers: try: identity_info = multipass.get_identity(provider, identifier) except IdentityRetrievalFailed: continue if identity_info is None: continue if not identity_info.provider.settings.get('trusted_email'): continue emails = { email.lower() for email in identity_info.data.getlist('email') if email } if not emails: continue user = User.find_first( ~User.is_deleted, User.all_emails.contains(db.func.any(list(emails)))) if user: return user
def get_event_person(event, data, create_untrusted_persons=False, allow_external=False, allow_emails=False, allow_networks=False): """Get an EventPerson from dictionary data. If there is already an event person in the same event and for the same user, it will be returned. Matching is done with the e-mail. """ person_type = data.get('_type') if person_type is None: if data.get('email'): email = data['email'].lower() user = User.find_first(~User.is_deleted, User.all_emails.contains(email)) if user: return get_event_person_for_user(event, user, create_untrusted_persons=create_untrusted_persons) elif event: person = event.persons.filter_by(email=email).first() if person: return person # We have no way to identify an existing event person with the provided information return create_event_person(event, create_untrusted_persons=create_untrusted_persons, **data) elif person_type == 'Avatar': # XXX: existing_data principal = principal_from_fossil(data, allow_pending=allow_external, allow_emails=allow_emails, allow_networks=allow_networks) return get_event_person_for_user(event, principal, create_untrusted_persons=create_untrusted_persons) elif person_type == 'EventPerson': return event.persons.filter_by(id=data['id']).one() elif person_type == 'PersonLink': return event.persons.filter_by(id=data['personId']).one() else: raise ValueError("Unknown person type '{}'".format(person_type))
def _process(self): if session.user: return redirect(url_for('misc.index')) handler = MultipassRegistrationHandler(self) if self.identity_info else LocalRegistrationHandler(self) verified_email = self._get_verified_email() if verified_email is not None: handler.email_verified(verified_email) flash(_('You have successfully validated your email address and can now proceeed with the registration.'), 'success') # Check whether there is already an existing pending user with this e-mail pending = User.find_first(User.all_emails.contains(verified_email), is_pending=True) if pending: session['register_pending_user'] = pending.id flash(_("There is already some information in Indico that concerns you. " "We are going to link it automatically."), 'info') return redirect(url_for('.register', provider=self.provider_name)) form = handler.create_form() if form.validate_on_submit(): if handler.must_verify_email: return self._send_confirmation(form.email.data) else: return self._create_user(form, handler) return WPAuth.render_template('register.html', form=form, local=(not self.identity_info), must_verify_email=handler.must_verify_email, widget_attrs=handler.widget_attrs, email_sent=session.pop('register_verification_email_sent', False))
def _process(self): if session.user: return redirect(url_for('misc.index')) handler = MultipassRegistrationHandler(self) if self.identity_info else LocalRegistrationHandler(self) verified_email = self._get_verified_email() if verified_email is not None: handler.email_verified(verified_email) flash(_('You have successfully validated your email address and can now proceeed with the registration.'), 'success') return redirect(url_for('.register', provider=self.provider_name)) form = handler.create_form() # Check for pending users if we have verified emails pending = None if not handler.must_verify_email: pending = User.find_first(~User.is_deleted, User.is_pending, User.all_emails.contains(db.func.any(list(handler.get_all_emails(form))))) if form.validate_on_submit(): if handler.must_verify_email: return self._send_confirmation(form.email.data) else: return self._create_user(form, handler, pending) elif not form.is_submitted() and pending: # If we have a pending user, populate empty fields with data from that user for field in form: value = getattr(pending, field.short_name, '') if value and not field.data: field.data = value if pending: flash(_("There is already some information in Indico that concerns you. " "We are going to link it automatically."), 'info') return WPAuth.render_template('register.html', form=form, local=(not self.identity_info), must_verify_email=handler.must_verify_email, widget_attrs=handler.widget_attrs, email_sent=session.pop('register_verification_email_sent', False))
def _original_user(self): # A proper user, with an id that can be mapped directly to sqlalchemy if isinstance(self.id, int) or self.id.isdigit(): return User.get(int(self.id)) # A user who had no real indico account but an ldap identifier/email. # In this case we try to find his real user and replace the ID of this object # with that user's ID. data = self.id.split(':') # TODO: Once everything is in SQLAlchemy this whole thing needs to go away! user = None if data[0] == 'LDAP': identifier = data[1] email = data[2] # You better have only one ldap provider or at least different identifiers ;) identity = Identity.find_first(Identity.provider != 'indico', Identity.identifier == identifier) if identity: user = identity.user elif data[0] == 'Nice': email = data[1] else: return None if not user: user = User.find_first(User.all_emails.contains(email)) if user: self._old_id = self.id self.id = str(user.id) logger.info("Updated legacy user id (%s => %s)", self._old_id, self.id) return user
def _original_user(self): # A proper user, with an id that can be mapped directly to sqlalchemy if isinstance(self.id, int) or self.id.isdigit(): return User.get(int(self.id)) # A user who had no real indico account but an ldap identifier/email. # In this case we try to find his real user and replace the ID of this object # with that user's ID. data = self.id.split(':') # TODO: Once everything is in SQLAlchemy this whole thing needs to go away! user = None if data[0] == 'LDAP': identifier = data[1] email = data[2] # You better have only one ldap provider or at least different identifiers ;) identity = Identity.find_first(Identity.provider != 'indico', Identity.identifier == identifier) if identity: user = identity.user elif data[0] == 'Nice': email = data[1] else: return None if not user: user = User.find_first(User.all_emails.contains(email)) if user: self._old_id = self.id self.id = str(user.id) logger.info("Updated legacy user id (%s => %s)", self._old_id, self.id) return user
def __init__(self): self.closed = False self.xml_generator = XMLGen() self.xml_generator.initXml() self.xml_generator.openTag(b'collection', [[b'xmlns', b'http://www.loc.gov/MARC21/slim']]) # This is horrible. but refactoring all the code in the indico core would be just as bad. admin = User.find_first(is_admin=True) self.output_generator = outputGenerator(admin, self.xml_generator)
def __init__(self): self.closed = False self.xml_generator = XMLGen() self.xml_generator.initXml() self.xml_generator.openTag(b'collection', [[b'xmlns', b'http://www.loc.gov/MARC21/slim']]) # This is horrible. but refactoring all the code in the indico core would be just as bad. admin = User.find_first(is_admin=True) self.output_generator = outputGenerator(admin, self.xml_generator)
def principal_from_fossil(fossil, allow_pending=False, allow_groups=True, legacy=True, allow_missing_groups=False, allow_emails=False, allow_networks=False): from indico.modules.networks.models.networks import IPNetworkGroup from indico.modules.groups import GroupProxy from indico.modules.users import User type_ = fossil['_type'] id_ = fossil['id'] if type_ == 'Avatar': if isinstance(id_, int) or id_.isdigit(): # regular user user = User.get(int(id_)) elif allow_pending: data = GenericCache('pending_identities').get(id_) if not data: raise ValueError("Cannot find user '{}' in cache".format(id_)) data = {k: '' if v is None else v for (k, v) in data.items()} email = data['email'].lower() # check if there is not already a (pending) user with that e-mail # we need to check for non-pending users too since the search may # show a user from external results even though the email belongs # to an indico account in case some of the search criteria did not # match the indico account user = User.find_first(User.all_emails.contains(email), ~User.is_deleted) if not user: user = User(first_name=data.get('first_name') or '', last_name=data.get('last_name') or '', email=email, address=data.get('address', ''), phone=data.get('phone', ''), affiliation=data.get('affiliation', ''), is_pending=True) db.session.add(user) db.session.flush() else: raise ValueError("Id '{}' is not a number and allow_pending=False".format(id_)) if user is None: raise ValueError('User does not exist: {}'.format(id_)) return user.as_avatar if legacy else user elif allow_emails and type_ == 'Email': return EmailPrincipal(id_) elif allow_networks and type_ == 'IPNetworkGroup': group = IPNetworkGroup.get(int(id_)) if group is None: raise ValueError('IP network group does not exist: {}'.format(id_)) return group elif allow_groups and type_ in {'LocalGroupWrapper', 'LocalGroup'}: group = GroupProxy(int(id_)) if group.group is None: raise ValueError('Local group does not exist: {}'.format(id_)) return group.as_legacy_group if legacy else group elif allow_groups and type_ in {'LDAPGroupWrapper', 'MultipassGroup'}: provider = fossil['provider'] group = GroupProxy(id_, provider) if group.group is None and not allow_missing_groups: raise ValueError('Multipass group does not exist: {}:{}'.format(provider, id_)) return group.as_legacy_group if legacy else group else: raise ValueError('Unexpected fossil type: {}'.format(type_))
def principal_from_fossil(fossil, allow_pending=False, allow_groups=True, legacy=True, allow_missing_groups=False, allow_emails=False): """Gets a GroupWrapper or AvatarUserWrapper from a fossil""" from indico.modules.groups import GroupProxy from indico.modules.users import User type_ = fossil['_type'] id_ = fossil['id'] if type_ == 'Avatar': if isinstance(id_, int) or id_.isdigit(): # regular user user = User.get(int(id_)) elif allow_pending: data = GenericCache('pending_identities').get(id_) if not data: raise ValueError("Cannot find user '{}' in cache".format(id_)) data = {k: '' if v is None else v for (k, v) in data.items()} email = data['email'].lower() # check if there is not already a pending user with that e-mail user = User.find_first(email=email, is_pending=True) if not user: user = User(first_name=data.get('first_name') or '', last_name=data.get('last_name') or '', email=email, address=data.get('address', ''), phone=data.get('phone', ''), affiliation=data.get('affiliation', ''), is_pending=True) db.session.add(user) db.session.flush() else: raise ValueError( "Id '{}' is not a number and allow_pending=False".format(id_)) if user is None: raise ValueError('User does not exist: {}'.format(id_)) return user.as_avatar if legacy else user elif allow_emails and type_ == 'Email': return EmailPrincipal(id_) elif allow_groups and type_ in {'LocalGroupWrapper', 'LocalGroup'}: group = GroupProxy(int(id_)) if group.group is None: raise ValueError('Local group does not exist: {}'.format(id_)) return group.as_legacy_group if legacy else group elif allow_groups and type_ in {'LDAPGroupWrapper', 'MultipassGroup'}: provider = fossil['provider'] group = GroupProxy(id_, provider) if group.group is None and not allow_missing_groups: raise ValueError('Multipass group does not exist: {}:{}'.format( provider, id_)) return group.as_legacy_group if legacy else group else: raise ValueError('Unexpected fossil type: {}'.format(type_))
def convert_principal(self, old_principal): principal = convert_principal(old_principal) if (principal is None and old_principal.__class__.__name__ in ('Avatar', 'AvatarUserWrapper') and 'email' in old_principal.__dict__): email = convert_to_unicode(old_principal.__dict__['email']).lower() principal = User.find_first(~User.is_deleted, User.all_emails.contains(email)) if principal is not None: self.print_warning('Using {} for {} (matched via {})'.format(principal, old_principal, email)) return principal
def create_user(email, data, identity=None, settings=None, other_emails=None, from_moderation=True): """Create a new user. This may also convert a pending user to a proper user in case the email address matches such a user. :param email: The primary email address of the user. :param data: The data used to populate the user. :param identity: An `Identity` to associate with the user. :param settings: A dict containing user settings. :param other_emails: A set of email addresses that are also used to check for a pending user. They will also be added as secondary emails to the user. :param from_moderation: Whether the user was created through the moderation process or manually by an admin. """ if other_emails is None: other_emails = set() if settings is None: settings = {} settings.setdefault('timezone', config.DEFAULT_TIMEZONE) settings.setdefault('lang', config.DEFAULT_LOCALE) settings.setdefault('suggest_categories', False) # Get a pending user if there is one user = User.find_first( ~User.is_deleted, User.is_pending, User.all_emails.contains(db.func.any(list({email} | set(other_emails))))) if not user: user = User() if email in user.secondary_emails: # This can happen if there's a pending user who has a secondary email # for some weird reason which should now become the primary email... user.make_email_primary(email) else: user.email = email user.populate_from_dict(data) user.is_pending = False user.secondary_emails |= other_emails user.favorite_users.add(user) if identity is not None: user.identities.add(identity) db.session.add(user) user.settings.set_multi(settings) db.session.flush() signals.users.registered.send(user, from_moderation=from_moderation, identity=identity) db.session.flush() return user
def _process(self): if session.user: return redirect(url_for_index()) handler = MultipassRegistrationHandler( self) if self.identity_info else LocalRegistrationHandler(self) verified_email, prevalidated = self._get_verified_email() if verified_email is not None: handler.email_verified(verified_email) if prevalidated: flash( _("You may change your email address after finishing the registration process." ), 'info') else: flash( _('You have successfully validated your email address and can now proceeed with the ' 'registration.'), 'success') return redirect(url_for('.register', provider=self.provider_name)) form = handler.create_form() if not handler.moderate_registrations and not handler.must_verify_email: del form.comment # Check for pending users if we have verified emails pending = None if not handler.must_verify_email: pending = User.find_first( ~User.is_deleted, User.is_pending, User.all_emails.contains( db.func.any(list(handler.get_all_emails(form))))) if form.validate_on_submit(): if handler.must_verify_email: return self._send_confirmation(form.email.data) elif handler.moderate_registrations: return self._create_registration_request(form, handler) else: return self._create_user(form, handler) elif not form.is_submitted() and pending: # If we have a pending user, populate empty fields with data from that user for field in form: value = getattr(pending, field.short_name, '') if value and not field.data: field.data = value if pending: flash( _("There is already some information in Indico that concerns you. " "We are going to link it automatically."), 'info') return WPAuth.render_template( 'register.html', form=form, local=(not self.identity_info), must_verify_email=handler.must_verify_email, widget_attrs=handler.widget_attrs, email_sent=session.pop('register_verification_email_sent', False), moderate_accounts=handler.moderate_registrations)
def convert_principal(self, old_principal): principal = convert_principal(old_principal) if (principal is None and old_principal.__class__.__name__ in ('Avatar', 'AvatarUserWrapper') and 'email' in old_principal.__dict__): email = convert_to_unicode(old_principal.__dict__['email']).lower() principal = User.find_first(~User.is_deleted, User.all_emails.contains(email)) if principal is not None: self.print_warning('Using {} for {} (matched via {})'.format( principal, old_principal, email)) return principal
def _get_user_data(self): user_id = request.args.get('user') if user_id is None: return {} elif user_id.isdigit(): # existing indico user user = User.find_first(id=user_id, is_deleted=False) return {t.name: getattr(user, t.name, None) if user else '' for t in PersonalDataType} else: # non-indico user data = GenericCache('pending_identities').get(user_id, {}) return {t.name: data.get(t.name) for t in PersonalDataType}
def _get_user_data(self): user_id = request.args.get("user") if user_id is None: return {} elif user_id.isdigit(): # existing indico user user = User.find_first(id=user_id, is_deleted=False) user_data = {t.name: getattr(user, t.name, None) if user else "" for t in PersonalDataType} else: # non-indico user data = GenericCache("pending_identities").get(user_id, {}) user_data = {t.name: data.get(t.name) for t in PersonalDataType} user_data["title"] = get_title_uuid(self.regform, user_data["title"]) return user_data
def _get_user_data(self): user_id = request.args.get('user') if user_id is None: return {} elif user_id.isdigit(): # existing indico user user = User.find_first(id=user_id, is_deleted=False) user_data = {t.name: getattr(user, t.name, None) if user else '' for t in PersonalDataType} else: # non-indico user data = GenericCache('pending_identities').get(user_id, {}) user_data = {t.name: data.get(t.name) for t in PersonalDataType} user_data['title'] = get_title_uuid(self.regform, user_data['title']) return user_data
def principal_from_fossil(fossil, allow_pending=False, allow_groups=True, legacy=True, allow_missing_groups=False, allow_emails=False): """Gets a GroupWrapper or AvatarUserWrapper from a fossil""" from indico.modules.groups import GroupProxy from indico.modules.users import User type_ = fossil['_type'] id_ = fossil['id'] if type_ == 'Avatar': if isinstance(id_, int) or id_.isdigit(): # regular user user = User.get(int(id_)) elif allow_pending: data = GenericCache('pending_identities').get(id_) if not data: raise ValueError("Cannot find user '{}' in cache".format(id_)) data = {k: '' if v is None else v for (k, v) in data.items()} email = data['email'].lower() # check if there is not already a pending user with that e-mail user = User.find_first(email=email, is_pending=True) if not user: user = User(first_name=data.get('first_name') or '', last_name=data.get('last_name') or '', email=email, address=data.get('address', ''), phone=data.get('phone', ''), affiliation=data.get('affiliation', ''), is_pending=True) db.session.add(user) db.session.flush() else: raise ValueError("Id '{}' is not a number and allow_pending=False".format(id_)) if user is None: raise ValueError('User does not exist: {}'.format(id_)) return user.as_avatar if legacy else user elif allow_emails and type_ == 'Email': return EmailPrincipal(id_) elif allow_groups and type_ in {'LocalGroupWrapper', 'LocalGroup'}: group = GroupProxy(int(id_)) if group.group is None: raise ValueError('Local group does not exist: {}'.format(id_)) return group.as_legacy_group if legacy else group elif allow_groups and type_ in {'LDAPGroupWrapper', 'MultipassGroup'}: provider = fossil['provider'] group = GroupProxy(id_, provider) if group.group is None and not allow_missing_groups: raise ValueError('Multipass group does not exist: {}:{}'.format(provider, id_)) return group.as_legacy_group if legacy else group else: raise ValueError('Unexpected fossil type: {}'.format(type_))
def create_user(email, data, identity=None, settings=None, other_emails=None, from_moderation=True): """Create a new user. This may also convert a pending user to a proper user in case the email address matches such a user. :param email: The primary email address of the user. :param data: The data used to populate the user. :param identity: An `Identity` to associate with the user. :param settings: A dict containing user settings. :param other_emails: A set of email addresses that are also used to check for a pending user. They will also be added as secondary emails to the user. :param from_moderation: Whether the user was created through the moderation process or manually by an admin. """ if other_emails is None: other_emails = set() if settings is None: settings = {} settings.setdefault('timezone', config.DEFAULT_TIMEZONE) settings.setdefault('lang', config.DEFAULT_LOCALE) settings.setdefault('suggest_categories', False) # Get a pending user if there is one user = User.find_first(~User.is_deleted, User.is_pending, User.all_emails.contains(db.func.any(list({email} | set(other_emails))))) if not user: user = User() if email in user.secondary_emails: # This can happen if there's a pending user who has a secondary email # for some weird reason which should now become the primary email... user.make_email_primary(email) else: user.email = email user.populate_from_dict(data) user.is_pending = False user.secondary_emails |= other_emails user.favorite_users.add(user) if identity is not None: user.identities.add(identity) db.session.add(user) user.settings.set_multi(settings) db.session.flush() signals.users.registered.send(user, from_moderation=from_moderation, identity=identity) db.session.flush() return user
def get_user_from_identifier(settings, identifier): """Get an actual User object from an identifier""" providers = list(auth.strip() for auth in settings.get('authenticators').split(',')) identities = Identity.find_all(Identity.provider.in_(providers), Identity.identifier == identifier) if identities: return sorted(identities, key=lambda x: providers.index(x.provider))[0].user for provider in providers: try: identity_info = multipass.get_identity(provider, identifier) except IdentityRetrievalFailed: continue if identity_info is None: continue if not identity_info.provider.settings.get('trusted_email'): continue emails = {email.lower() for email in identity_info.data.getlist('email') if email} if not emails: continue user = User.find_first(~User.is_deleted, User.all_emails.in_(list(emails))) if user: return user
def _get_event_person(self, data): person_type = data.get('_type') if person_type is None: if data.get('email'): email = data['email'].lower() user = User.find_first(~User.is_deleted, User.all_emails.contains(email)) if user: return self._get_event_person_for_user(user) elif self.event: person = self.event.persons.filter_by(email=email).first() if person: return person # We have no way to identify an existing event person with the provided information return self._create_event_person(data) elif person_type == 'Avatar': return self._get_event_person_for_user(self._convert_principal(data)) elif person_type == 'EventPerson': return self.event.persons.filter_by(id=data['id']).one() elif person_type == 'PersonLink': return self.event.persons.filter_by(id=data['personId']).one() else: raise ValueError(_("Unknown person type '{}'").format(person_type))
def _get_event_person(self, data): person_type = data.get('_type') if person_type is None: if data.get('email'): email = data['email'].lower() user = User.find_first(~User.is_deleted, User.all_emails.contains(email)) if user: return self._get_event_person_for_user(user) elif self.event: person = self.event.persons.filter_by(email=email).first() if person: return person # We have no way to identify an existing event person with the provided information return self._create_event_person(data) elif person_type == 'Avatar': return self._get_event_person_for_user( self._convert_principal(data)) elif person_type == 'EventPerson': return self.event.persons.filter_by(id=data['id']).one() elif person_type == 'PersonLink': return self.event.persons.filter_by(id=data['personId']).one() else: raise ValueError(_("Unknown person type '{}'").format(person_type))
def user(self): from indico.modules.users import User return User.find_first(~User.is_deleted, User.all_emails.contains(self.email))
def _checkParams(self, params): RHManageRegFormBase._checkParams(self, params) user_id = request.args.get('user') self.user = User.find_first(User.id == user_id, ~User.is_deleted) if user_id else None
def user(self): if not self.is_submitted() or not self.email.data: return None return User.find_first(~User.is_deleted, ~User.is_blocked, ~User.is_pending, User.all_emails.contains(self.email.data))
def principal_from_fossil(fossil, allow_pending=False, allow_groups=True, allow_missing_groups=False, allow_emails=False, allow_networks=False, existing_data=None, event=None): from indico.modules.networks.models.networks import IPNetworkGroup from indico.modules.events.models.roles import EventRole from indico.modules.groups import GroupProxy from indico.modules.users import User if existing_data is None: existing_data = set() type_ = fossil['_type'] id_ = fossil['id'] if type_ == 'Avatar': if isinstance(id_, int) or id_.isdigit(): # regular user user = User.get(int(id_)) elif allow_pending: data = GenericCache('pending_identities').get(id_) if not data: raise ValueError("Cannot find user '{}' in cache".format(id_)) data = {k: '' if v is None else v for k, v in data.items()} email = data['email'].lower() # check if there is not already a (pending) user with that e-mail # we need to check for non-pending users too since the search may # show a user from external results even though the email belongs # to an indico account in case some of the search criteria did not # match the indico account user = User.find_first(User.all_emails.contains(email), ~User.is_deleted) if not user: user = User(first_name=data.get('first_name') or '', last_name=data.get('last_name') or '', email=email, address=data.get('address', ''), phone=data.get('phone', ''), affiliation=data.get('affiliation', ''), is_pending=True) db.session.add(user) db.session.flush() else: raise ValueError("Id '{}' is not a number and allow_pending=False".format(id_)) if user is None: raise ValueError('User does not exist: {}'.format(id_)) return user elif allow_emails and type_ == 'Email': return EmailPrincipal(id_) elif allow_networks and type_ == 'IPNetworkGroup': group = IPNetworkGroup.get(int(id_)) if group is None or (group.hidden and group not in existing_data): raise ValueError('IP network group does not exist: {}'.format(id_)) return group elif allow_groups and type_ in {'LocalGroupWrapper', 'LocalGroup'}: group = GroupProxy(int(id_)) if group.group is None: raise ValueError('Local group does not exist: {}'.format(id_)) return group elif allow_groups and type_ in {'LDAPGroupWrapper', 'MultipassGroup'}: provider = fossil['provider'] group = GroupProxy(id_, provider) if group.group is None and not allow_missing_groups: raise ValueError('Multipass group does not exist: {}:{}'.format(provider, id_)) return group elif event and type_ == 'EventRole': role = EventRole.get(id_) role_name = fossil.get('name') if role is None: raise ValueError('Role does not exist: {}:{}'.format(role_name, id_)) if role.event != event: raise ValueError('Role does not belong to provided event: {}:{} - {}'.format(role_name, id_, event)) return role else: raise ValueError('Unexpected fossil type: {}'.format(type_))
def user(self): from indico.modules.users import User return User.find_first(~User.is_deleted, User.all_emails.contains(self.email))
def user(self): if not self.is_submitted() or not self.email.data: return None return User.find_first(~User.is_deleted, ~User.is_blocked, ~User.is_pending, User.all_emails.contains(self.email.data))
def _checkParams(self, params): RHManageRegFormBase._checkParams(self, params) user_id = request.args.get('user') self.user = User.find_first(User.id == user_id, ~User.is_deleted) if user_id else None