def property_intervals(self, name, when=UnboundedInterval): """ Get the set of intervals in which the user was granted a given property :param str name: :param Interval when: :returns: The set of intervals in which the user was granted the property :rtype: IntervalSet """ property_assignments = object_session(self).query( Property.granted, Membership.begins_at, Membership.ends_at ).filter( Property.name == name, Property.property_group_id == PropertyGroup.id, PropertyGroup.id == Membership.group_id, Membership.user_id == self.id ).all() granted_intervals = IntervalSet( closed(begins_at, ends_at) for granted, begins_at, ends_at in property_assignments if granted ) denied_intervals = IntervalSet( closed(begins_at, ends_at) for granted, begins_at, ends_at in property_assignments if not granted ) return (granted_intervals - denied_intervals).intersect(when)
def sync_exceeded_traffic_limits(): """Adds and removes memberships of the 'traffic_limit_exceeded group.' """ processor = User.q.get(0) # Add memberships users = User.q.join(User._current_traffic_balance) \ .filter(CurrentTrafficBalance.amount < 0) \ .except_(User.q.join(User.current_properties)\ .filter(or_(CurrentProperty.property_name == 'traffic_limit_disabled',\ CurrentProperty.property_name == 'traffic_limit_exceeded')))\ .all() for user in users: make_member_of(user, config.traffic_limit_exceeded_group, processor, closed(session.utcnow(), None)) print("Traffic-Limit exceeded: " + user.name) # End memberships users = User.q.join(User.current_properties) \ .filter(CurrentProperty.property_name == 'traffic_limit_exceeded') \ .join(User._current_traffic_balance) \ .filter(or_(CurrentTrafficBalance.amount >= 0, CurrentProperty.property_name == 'traffic_limit_disabled')) \ .all() for user in users: remove_member_of(user, config.traffic_limit_exceeded_group, processor, closed(session.utcnow(), None)) print("Traffic-Limit no longer exceeded: " + user.name)
def test_complement(self): self.assertIntervalSetOperationEquals(IntervalSet.complement, [ ([[]], open(None, None)), ([[closed(0, 1), open(2, 3)] ], [open(None, 0), openclosed(1, 2), closedopen(3, None)]), ([[closed(None, 0), closed(1, None)]], ([open(0, 1)])), ])
def test_length(self): self.assertIntervalSetMethodEquals(operator.attrgetter("length"), [ ([[closed(0, 0)]], 0), ([[closed(0, 1)]], 1), ([[closed(0, 0), closed(1, 2), closed(3, 4)]], 2), ([[closed(0, 1), closed(2, None)]], None), ([[closed(None, 0), closed(1, 2)]], None), ([[closed(None, None)]], None), ])
def test_intersect(self): self.assertIntervalSetOperationEquals(IntervalSet.intersect, [ ([[open(None, None)], [openclosed(None, 0), closed(1, 2), closedopen(3, None)] ], [openclosed(None, 0), closed(1, 2), closedopen(3, None)]), ])
def test_length(self): self.assertCallEquals(operator.attrgetter("length"), [ ([open(0, 0)], 0), ([closed(0, 0)], 0), ([open(0, 1)], 1), ([closed(0, 1)], 1), ([closed(0, None)], None), ([closed(None, 0)], None), ([closed(None, None)], None), ])
def test_join_overlapping_memberships(self): begins_at1 = session.utcnow() ends_at1 = begins_at1 + timedelta(hours=2) during1 = closed(begins_at1, ends_at1) begins_at2 = begins_at1 + timedelta(hours=1) ends_at2 = begins_at1 + timedelta(hours=3) during2 = closed(begins_at2, ends_at2) self.add_membership(during1) self.add_membership(during2) self.assertMembershipIntervalsEqual(IntervalSet(closed(begins_at1, ends_at2)))
def test_removing_all_memberships(self): begins_at1 = session.utcnow() ends_at1 = begins_at1 + timedelta(hours=1) during1 = closed(begins_at1, ends_at1) begins_at2 = begins_at1 + timedelta(hours=2) ends_at2 = begins_at1 + timedelta(hours=3) during2 = closed(begins_at2, ends_at2) self.add_membership(during1) self.add_membership(during2) self.remove_membership() self.assertMembershipIntervalsEqual(IntervalSet())
def test_removing_memberships(self): t0 = session.utcnow() t1 = t0 + timedelta(hours=1) t2 = t0 + timedelta(hours=2) t3 = t0 + timedelta(hours=3) t4 = t0 + timedelta(hours=4) t5 = t0 + timedelta(hours=5) self.add_membership(closed(t0, t2)) self.add_membership(closed(t3, t5)) self.remove_membership(closed(t1, t4)) self.assertMembershipIntervalsEqual(IntervalSet( (closed(t0, t1), closed(t4, t5))))
def test_contains_operator(self): self.assertCallTrue(operator.contains, [ (closed(0, 0), 0), (closed(0, 2), 1), (openclosed(None, 0), 0), (closedopen(0, None), 0), (open(None, None), 0), ]) self.assertCallFalse(operator.contains, [ (empty(0), 0), (closedopen(0, 1), 1), (openclosed(0, 1), 0), (open(0, 1), 1), ])
def test_difference(self): self.assertIntervalSetOperationEquals(IntervalSet.difference, [ ([[open(None, None)], [closed(0, 1), closedopen(2, 3), openclosed(4, 5), open(6, 7)]], [ open(None, 0), open(1, 2), closed(3, 4), openclosed(5, 6), closedopen(7, None) ]), ])
def test_empty(self): self.assertCallTrue(operator.attrgetter("empty"), [ [empty(0)], [closedopen(0, 0)], [openclosed(0, 0)], [open(0, 0)], ]) self.assertCallFalse(operator.attrgetter("empty"), [ [single(0)], [closed(0, 0)], [closed(0, 1)], [closedopen(0, 1)], [openclosed(0, 1)], [open(0, 1)], ])
def test_adding_single_membership(self): begins_at = session.utcnow() ends_at = begins_at + timedelta(hours=1) during = closed(begins_at, ends_at) self.add_membership(during) self.assertMembershipIntervalsEqual(IntervalSet(during))
def make_member_of(user, group, processor, during=UnboundedInterval): """ Makes a user member of a group in a given interval. If the given interval overlaps with an existing membership, this method will join the overlapping intervals together, so that there will be at most one membership for particular user in particular group at any given point in time. :param User user: the user :param Group group: the group :param User processor: User issuing the addition :param Interval during: """ memberships = session.session.query(Membership).filter( Membership.user == user, Membership.group == group, Membership.active(during)).all() intervals = IntervalSet( closed(m.begins_at, m.ends_at) for m in memberships).union(during) for m in memberships: session.session.delete(m) session.session.add_all( Membership(begins_at=i.begin, ends_at=i.end, user=user, group=group) for i in intervals) message = deferred_gettext(u"Added to group {group} during {during}.") log_user_event(message=message.format(group=group.name, during=during).to_json(), user=user, author=processor)
def remove_member_of(user, group, processor, during=UnboundedInterval): """Remove a user from a group in a given interval. The interval defaults to the unbounded interval, so that the user will be removed from the group at any point in time, **removing all memberships** in this group retroactively. However, a common use case is terminating a membership by setting ``during=closedopen(now, None)``. :param User user: the user :param Group group: the group :param User processor: User issuing the removal :param Interval during: """ memberships = session.session.query(Membership).filter( Membership.user == user, Membership.group == group, Membership.active(during)).all() intervals = IntervalSet( closed(m.begins_at, m.ends_at) for m in memberships ).difference(during) for m in memberships: session.session.delete(m) session.session.add_all( Membership(begins_at=i.begin, ends_at=i.end, user=user, group=group) for i in intervals) message = deferred_gettext(u"Removed from group {group} during {during}.") log_user_event(message=message.format(group=group.name, during=during).to_json(), user=user, author=processor)
def make_member_of(user, group, processor, during=UnboundedInterval): """ Makes a user member of a group in a given interval. If the given interval overlaps with an existing membership, this method will join the overlapping intervals together, so that there will be at most one membership for particular user in particular group at any given point in time. :param User user: the user :param Group group: the group :param User processor: User issuing the addition :param Interval during: """ memberships = session.session.query(Membership).filter( Membership.user == user, Membership.group == group, Membership.active(during)).all() intervals = IntervalSet( closed(m.begins_at, m.ends_at) for m in memberships ).union(during) for m in memberships: session.session.delete(m) session.session.add_all( Membership(begins_at=i.begin, ends_at=i.end, user=user, group=group) for i in intervals) message = deferred_gettext(u"Added to group {group} during {during}.") log_user_event(message=message.format(group=group.name, during=during).to_json(), user=user, author=processor)
def add_membership(user_id): user = get_user_or_404(user_id) form = UserAddGroupMembership() if form.validate_on_submit(): if form.begins_at.data is not None: begins_at = datetime.combine(form.begins_at.data, utc.time_min()) else: begins_at = session.utcnow() if not form.ends_at.unlimited.data: ends_at = datetime.combine(form.ends_at.date.data, utc.time_min()) else: ends_at = None make_member_of(user, form.group.data, current_user, closed(begins_at, ends_at)) message = u"Nutzer zur Gruppe '{}' hinzugefügt.".format(form.group.data.name) lib.logging.log_user_event(message, current_user, user) session.session.commit() flash(u'Nutzer wurde der Gruppe hinzugefügt.', 'success') return redirect(url_for(".user_show", user_id=user_id, _anchor='groups')) return render_template('user/add_membership.html', page_title=u"Neue Gruppenmitgliedschaft für Nutzer {}".format(user_id), user_id=user_id, form=form)
def assertMembershipIntervalsEqual(self, expected): memberships = session.session.query(Membership).filter_by( user=self.user, group=self.group) got = IntervalSet(closed(m.begins_at, m.ends_at) for m in memberships) assert expected == got, "IntervalSets differ: " \ "expected {0!r}" \ "got {1!r}".format(expected, got)
def remove_member_of(user, group, processor, during=UnboundedInterval): """Remove a user from a group in a given interval. The interval defaults to the unbounded interval, so that the user will be removed from the group at any point in time, **removing all memberships** in this group retroactively. However, a common use case is terminating a membership by setting ``during=closedopen(now, None)``. :param User user: the user :param Group group: the group :param User processor: User issuing the removal :param Interval during: """ memberships = session.session.query(Membership).filter( Membership.user == user, Membership.group == group, Membership.active(during)).all() intervals = IntervalSet( closed(m.begins_at, m.ends_at) for m in memberships).difference(during) for m in memberships: session.session.delete(m) session.session.add_all( Membership(begins_at=i.begin, ends_at=i.end, user=user, group=group) for i in intervals) message = deferred_gettext(u"Removed from group {group} during {during}.") log_user_event(message=message.format(group=group.name, during=during).to_json(), user=user, author=processor)
def test_positive_test_interval(self): interval = closed(self.membership.begins_at, self.membership.ends_at) self.assertTrue(self.user.has_property(self.GRANTED_NAME, interval)) self.assertIsNotNone( user.User.q.filter( user.User.login == self.user.login, user.User.has_property(self.GRANTED_NAME, interval)).first())
def test_serialize_interval(self): self.assertValidSerialization(UnboundedInterval) now = datetime.datetime.utcnow() then = now + datetime.timedelta(1) self.assertValidSerialization(closed(now, then)) self.assertValidSerialization(closedopen(now, then)) self.assertValidSerialization(openclosed(now, then)) self.assertValidSerialization(open(now, then))
def test_negative_test_interval(self): interval = closed(self.membership.ends_at + timedelta(1), self.membership.ends_at + timedelta(2)) self.assertFalse(self.user.has_property(self.GRANTED_NAME, interval)) self.assertIsNone( user.User.q.filter( user.User.login == self.user.login, user.User.has_property(self.GRANTED_NAME, interval)).first())
def active_memberships(self, when=None): if when is None: now = session.utcnow() when = single(now) return [ m for m in self.memberships if when.overlaps(closed(m.begins_at, m.ends_at)) ]
def edit_membership(user_id, membership_id): membership = Membership.q.get(membership_id) if membership is None: flash( u"Gruppenmitgliedschaft mit ID {} existiert nicht!".format( membership_id), 'error') abort(404) if membership.group.permission_level > current_user.permission_level: flash( "Eine Bearbeitung von Gruppenmitgliedschaften für Gruppen mit " "höherem Berechtigungslevel ist nicht möglich.", 'error') abort(403) membership_data = {} if request.method == 'GET': membership_data = { "begins_at": None if membership.begins_at is None else membership.begins_at.date(), "ends_at": { "unlimited": membership.ends_at is None, "date": membership.ends_at and membership.ends_at.date() } } form = UserEditGroupMembership(**membership_data) if form.validate_on_submit(): membership.begins_at = datetime.combine(form.begins_at.data, utc.time_min()) if form.ends_at.unlimited.data: membership.ends_at = None else: membership.ends_at = datetime.combine(form.ends_at.date.data, utc.time_min()) message = ( u"Edited the membership of group '{group}'. During: {during}". format(group=membership.group.name, during=closed(membership.begins_at, membership.ends_at))) lib.logging.log_user_event(message, current_user, membership.user) session.session.commit() flash(u'Gruppenmitgliedschaft bearbeitet', 'success') return redirect( url_for('.user_show', user_id=membership.user_id, _anchor='groups')) return render_template('user/user_edit_membership.html', page_title=(u"Mitgliedschaft {} für " u"{} bearbeiten".format( membership.group.name, membership.user.name)), membership_id=membership_id, user=membership.user, form=form)
def test_union(self): self.assertIntervalSetOperationEquals(IntervalSet.union, [ ([[], [closed(0, 1), open(1, 2)]], [closed(0, 1), open(1, 2)]), ([[closed(0, 1), open(1, 2)], []], [closed(0, 1), open(1, 2)]), ([[closed(None, 1), closed(3, 4), open(7, 8)], [open(0, 5), closed(6, 7), closedopen(8, None)]], [open(None, 5), closed(6, None)]), ])
def active_memberships(self, when: Optional[Interval] = None ) -> List[Membership]: if when is None: now = session.utcnow() when = single(now) return [ m for m in self.memberships if when.overlaps(closed(m.begins_at, m.ends_at)) ]
def test_positive_test_interval(self): interval = closed(MembershipData.dummy_membership.begins_at, MembershipData.dummy_membership.ends_at) self.assertTrue( self.user.has_property(PropertyData.granted.name, interval)) self.assertIsNotNone( user.User.q.filter( user.User.login == self.user.login, user.User.has_property(PropertyData.granted.name, interval)).first())
def test_positive_test_interval(self): interval = closed(MembershipData.dummy_membership.begins_at, MembershipData.dummy_membership.ends_at) self.assertTrue( self.user.has_property(PropertyData.granted.name, interval) ) self.assertIsNotNone( user.User.q.filter( user.User.login == self.user.login, user.User.has_property(PropertyData.granted.name, interval) ).first())
def test_negative_test_interval(self): interval = closed( MembershipData.dummy_membership.ends_at + timedelta(1), MembershipData.dummy_membership.ends_at + timedelta(2)) self.assertFalse( self.user.has_property(PropertyData.granted.name, interval)) self.assertIsNone( user.User.q.filter( user.User.login == self.user.login, user.User.has_property(PropertyData.granted.name, interval)).first())
def active(self, when=None): """ Tests if the membership overlaps with a given interval. If no interval is given, it tests if the membership is active right now. :param Interval when: interval in which the membership :rtype: bool """ if when is None: now = object_session(self).query(func.current_timestamp()).scalar() when = single(now) return when.overlaps(closed(self.begins_at, self.ends_at))
def test_negative_test_interval(self): interval = closed( MembershipData.dummy_membership.ends_at + timedelta(1), MembershipData.dummy_membership.ends_at + timedelta(2) ) self.assertFalse( self.user.has_property(PropertyData.granted.name, interval) ) self.assertIsNone( user.User.q.filter( user.User.login == self.user.login, user.User.has_property(PropertyData.granted.name, interval) ).first())
def property_intervals(self, name, when=UnboundedInterval): """ Get the set of intervals in which the user was granted a given property :param str name: :param Interval when: :returns: The set of intervals in which the user was granted the property :rtype: IntervalSet """ property_assignments = object_session(self).query( Property.granted, Membership.begins_at, Membership.ends_at).filter( Property.name == name, Property.property_group_id == PropertyGroup.id, PropertyGroup.id == Membership.group_id, Membership.user_id == self.id).all() granted_intervals = IntervalSet( closed(begins_at, ends_at) for granted, begins_at, ends_at in property_assignments if granted) denied_intervals = IntervalSet( closed(begins_at, ends_at) for granted, begins_at, ends_at in property_assignments if not granted) return (granted_intervals - denied_intervals).intersect(when)
def handle_payments_in_default(): processor = User.q.get(0) # Add memberships and end "member" membership if threshold met users = User.q.join(User.current_properties)\ .filter(CurrentProperty.property_name == 'membership_fee') \ .join(Account).filter(Account.balance > 0).all() users_pid_membership = [] users_membership_terminated = [] ts_now = session.utcnow() for user in users: last_pid_membership = Membership.q.filter(Membership.user_id == user.id) \ .filter(Membership.group_id == config.payment_in_default_group.id) \ .order_by(Membership.ends_at.desc()) \ .first() if last_pid_membership is not None: if last_pid_membership.ends_at is not None and \ last_pid_membership.ends_at >= ts_now - timedelta(days=7): continue in_default_days = user.account.in_default_days try: fee = get_membership_fee_for_date( date.today() - timedelta(days=in_default_days)) except NoResultFound: fee = get_last_applied_membership_fee() if not fee: return [], [] if not user.has_property('payment_in_default'): if in_default_days >= fee.payment_deadline.days: make_member_of(user, config.payment_in_default_group, processor, closed(ts_now, None)) users_pid_membership.append(user) if in_default_days >= fee.payment_deadline_final.days: remove_member_of(user, config.member_group, processor, closedopen(ts_now, None)) log_user_event("Mitgliedschaftsende wegen Zahlungsrückstand ({})" .format(fee.name), processor, user) users_membership_terminated.append(user) return users_pid_membership, users_membership_terminated
def handle_payments_in_default(): processor = User.q.get(0) # Add memberships and end "member" membership if threshold met users = User.q.join(User.current_properties)\ .filter(CurrentProperty.property_name == 'membership_fee') \ .join(Account).filter(Account.balance > 0).all() users_pid_membership = [] users_membership_terminated = [] ts_now = session.utcnow() for user in users: last_pid_membership = Membership.q.filter(Membership.user_id == user.id) \ .filter(Membership.group_id == config.payment_in_default_group.id) \ .order_by(Membership.ends_at.desc()) \ .first() if last_pid_membership is not None: if last_pid_membership.ends_at is not None and \ last_pid_membership.ends_at >= ts_now - timedelta(days=7): continue in_default_days = user.account.in_default_days try: fee = get_membership_fee_for_date(date.today() - timedelta(days=in_default_days)) except NoResultFound: fee = get_last_applied_membership_fee() if not fee: return [], [] if not user.has_property('payment_in_default'): if in_default_days >= fee.payment_deadline.days: make_member_of(user, config.payment_in_default_group, processor, closed(ts_now, None)) users_pid_membership.append(user) if in_default_days >= fee.payment_deadline_final.days: remove_member_of(user, config.member_group, processor, closedopen(ts_now, None)) log_user_event( "Mitgliedschaftsende wegen Zahlungsrückstand ({})".format( fee.name), processor, user) users_membership_terminated.append(user) return users_pid_membership, users_membership_terminated
def finish_member_request(prm: PreMember, processor: User | None, ignore_similar_name: bool = False): if prm.room is None: raise ValueError("Room is None") if prm.move_in_date is not None and prm.move_in_date < session.utcnow( ).date(): prm.move_in_date = session.utcnow().date() check_new_user_data(prm.login, prm.email, prm.name, prm.swdd_person_id, prm.room, prm.move_in_date, ignore_similar_name) user, _ = create_user(prm.name, prm.login, prm.email, prm.birthdate, groups=[], processor=processor, address=prm.room.address, passwd_hash=prm.passwd_hash) processor = processor if processor is not None else user user.swdd_person_id = prm.swdd_person_id user.email_confirmed = prm.email_confirmed move_in_datetime = utc.with_min_time(prm.move_in_date) move_in(user, prm.room.building_id, prm.room.level, prm.room.number, None, processor if processor is not None else user, when=move_in_datetime) message = deferred_gettext("Created from registration {}.").format( str(prm.id)).to_json() log_user_event(message, processor, user) if move_in_datetime > session.utcnow(): make_member_of(user, config.pre_member_group, processor, closed(session.utcnow(), None)) session.session.delete(prm) return user
def create_user(name, login, email, birthdate, groups, processor, address): """Create a new member Create a new user with a generated password, finance- and unix account, and make him member of the `config.member_group` and `config.network_access_group`. :param str name: The full name of the user (e.g. Max Mustermann) :param str login: The unix login for the user :param str email: E-Mail address of the user :param Date birthdate: Date of birth :param PropertyGroup groups: The initial groups of the new user :param User processor: The processor :param Address address: Where the user lives. May or may not come from a room. :return: """ now = session.utcnow() plain_password = user_helper.generate_password(12) # create a new user new_user = User( login=login, name=name, email=email, registered_at=now, account=Account(name="", type="USER_ASSET"), password=plain_password, birthdate=birthdate, address=address ) account = UnixAccount(home_directory="/home/{}".format(login)) new_user.unix_account = account with session.session.begin(subtransactions=True): session.session.add(new_user) session.session.add(account) new_user.account.name = deferred_gettext(u"User {id}").format( id=new_user.id).to_json() for group in groups: make_member_of(new_user, group, processor, closed(now, None)) log_user_event(author=processor, message=deferred_gettext(u"User created.").to_json(), user=new_user) return new_user, plain_password
def take_actions_for_payment_in_default_users(users_pid_membership, users_membership_terminated, processor): ts_now = session.utcnow() for user in users_pid_membership: if not user.member_of(config.payment_in_default_group): make_member_of(user, config.payment_in_default_group, processor, closed(ts_now, None)) from pycroft.lib.user import move_out for user in users_membership_terminated: if user.member_of(config.member_group): move_out(user, "Zahlungsrückstand", processor, ts_now - timedelta(seconds=1), True) log_user_event("Mitgliedschaftsende wegen Zahlungsrückstand.", processor, user)
def __contains__(self, date): return date in closed(self.begins_on, self.ends_on)
def active_memberships(self, when=None): if when is None: now = session.utcnow() when = single(now) return [m for m in self.memberships if when.overlaps(closed(m.begins_at, m.ends_at))]