def test_get_machine_type(self): for machine_type in MachineTypeField.possible_machine_types: self.assertEqual( machine_type, MachineTypeField.get_machine_type(machine_type.id)) self.assertEqual(None, MachineTypeField.get_machine_type(0))
def test_get_prep_value(self): machine_type_field = MachineTypeField() self.assertEqual(None, machine_type_field.get_prep_value(None)) for machine_type in MachineTypeField.possible_machine_types: self.assertEqual(machine_type.id, machine_type_field.get_prep_value(machine_type)) self.assertEqual(machine_type.id, machine_type_field.get_prep_value([machine_type]))
def setUp(self): event = Event.objects.create(title="TEST EVENT") self.timeplace = TimePlace.objects.create( pub_date=timezone.now(), start_date=timezone.now(), start_time=(timezone.now() + timedelta(seconds=1)).time(), event=event) self.machine_type = MachineTypeField.get_machine_type(1) self.machine = Machine.objects.create(name="C1", location="Printer room", status="F", machine_type=self.machine_type) self.user = User.objects.create_user("User", "*****@*****.**", "user_pass") self.user_quota = Quota.objects.create(user=self.user, ignore_rules=False, number_of_reservations=2, machine_type=self.machine_type) self.course_registration = Printer3DCourse.objects.create( user=self.user, username=self.user.username, date=datetime.now().date(), name=self.user.get_full_name()) self.max_time_reservation = 5 ReservationRule.objects.create( machine_type=self.machine_type, start_time=time(0, 0), end_time=time(23, 59), days_changed=6, start_days=1, max_hours=self.max_time_reservation, max_inside_border_crossed=self.max_time_reservation)
class MachineUsageRule(models.Model): """ Allows for specification of rules for each type of machine """ machine_type = MachineTypeField(unique=True) content = RichTextUploadingField() content_en = RichTextUploadingField()
def test_calendar_reservation_url(self, now_mock): now_mock.return_value = local_to_date( timezone.datetime(2018, 12, 9, 12, 24)) user = User.objects.create_user("user", "*****@*****.**", "weak_pass") user.save() machine_type_printer = MachineTypeField.get_machine_type(1) Quota.objects.create(user=user, number_of_reservations=2, ignore_rules=True, machine_type=machine_type_printer) printer = Machine.objects.create(name="U1", location="S1", machine_model="Ultimaker", status="F", machine_type=machine_type_printer) Printer3DCourse.objects.create(user=user, username=user.username, name=user.get_full_name(), date=timezone.now()) reservation = Reservation.objects.create(user=user, machine=printer, event=None, start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=2)) self.assertEqual(current_calendar_url(printer), calendar_url_reservation(reservation))
def test_get_user_reservations_different_types(self): machine_type_sewing = MachineTypeField.get_machine_type(2) Quota.objects.create(user=self.user, number_of_reservations=2, machine_type=machine_type_sewing, ignore_rules=True) sewing_machine = Machine.objects.create( name="T1", machine_model="Generic", location="M1", status="F", machine_type=machine_type_sewing) Reservation.objects.create(user=self.user, machine=sewing_machine, start_time=timezone.now(), end_time=timezone.now() + timezone.timedelta(hours=2), event=None) self.assertEqual([ Reservation.objects.get(user=self.user, machine__machine_type=machine_type_sewing), Reservation.objects.get( user=self.user, machine__machine_type=self.machine_type_printer) ], list( template_view_get_context_data( MyReservationsView, request_user=self.user)["reservations"]))
def setUp(self): self.user = User.objects.create_user("user", "*****@*****.**") self.machine_type_printer = MachineTypeField.get_machine_type(1) printer = Machine.objects.create( name="U1", machine_type=self.machine_type_printer, machine_model="Ultimaker 2", location="S1", status="F") Quota.objects.create(user=self.user, number_of_reservations=2, ignore_rules=True, machine_type=self.machine_type_printer) Printer3DCourse.objects.create(user=self.user, name=self.user.get_full_name(), username=self.user.username, date=timezone.now().date()) Reservation.objects.create(user=self.user, machine=printer, event=None, start_time=timezone.now(), end_time=timezone.now() + timezone.timedelta(hours=2))
class Machine(models.Model): status_choices = ( ("R", _("Reserved")), ("F", _("Available")), ("I", _("In use")), ("O", _("Out of order")), ("M", _("Maintenance")), ) status = models.CharField(max_length=2, choices=status_choices, verbose_name=_("Status"), default="F") name = models.CharField(max_length=30, unique=True, verbose_name=_("Name")) location = models.CharField(max_length=40, verbose_name=_("Location")) location_url = models.URLField(verbose_name=_("Location URL")) machine_model = models.CharField(max_length=40, verbose_name=_("Machine model")) machine_type = MachineTypeField(null=True, verbose_name=_("Machine type")) @abstractmethod def get_reservation_set(self): return Reservation.objects.filter(machine=self) def get_next_reservation(self): return self.get_reservation_set().filter( start_time__gt=timezone.now()).order_by('start_time').first() @abstractmethod def can_user_use(self, user): return self.machine_type.can_user_use(user) def reservations_in_period(self, start_time, end_time): return self.get_reservation_set().filter(start_time__lte=start_time, end_time__gt=start_time) | \ self.get_reservation_set().filter(start_time__gte=start_time, end_time__lte=end_time) | \ self.get_reservation_set().filter(start_time__lt=end_time, start_time__gt=start_time, end_time__gte=end_time) def __str__(self): return self.name + " - " + self.machine_model def get_status(self): if self.status in "OM": return self.status return self.reservations_in_period( timezone.now(), timezone.now() + timedelta(seconds=1)) and "R" or "F" def _get_FIELD_display(self, field): if field.attname == "status": return self._get_status_display() return super()._get_FIELD_display(field) def _get_status_display(self): current_status = self.get_status() return [ full_name for short_hand, full_name in self.status_choices if short_hand == current_status ][0]
def setUp(self): machine_type_printer = MachineTypeField.get_machine_type(1) self.machine = Machine.objects.create( name="Test", machine_model="Ultimaker 2+", machine_type=machine_type_printer) self.event = Event.objects.create(title="Test_Event") self.timeplace = TimePlace.objects.create(event=self.event)
def setUp(self): self.machine_type_printer = MachineTypeField.get_machine_type(1) self.machine_type_sewing = MachineTypeField.get_machine_type(2) self.user = User.objects.create_user("test") self.user2 = User.objects.create_user("test2") self.quota1 = Quota.objects.create( all=True, machine_type=self.machine_type_sewing, number_of_reservations=2) self.quota2 = Quota.objects.create( all=True, machine_type=self.machine_type_printer, number_of_reservations=4) self.quota3 = Quota.objects.create( user=self.user, machine_type=self.machine_type_printer, number_of_reservations=4) self.quota4 = Quota.objects.create( user=self.user2, machine_type=self.machine_type_sewing, number_of_reservations=1)
def create_reservation(start_time, end_time): machine_type = MachineTypeField.get_machine_type(1) Machine.objects.create(name="S1", location="U1", machine_model="Ultimaker", status="F", machine_type=machine_type) user = User.objects.create_user("User", "*****@*****.**", "unsecure_pass") user.save() Quota.objects.create(user=user, number_of_reservations=2, ignore_rules=True, machine_type=machine_type) Printer3DCourse.objects.create(user=user, username=user.username, name=user.get_full_name(), date=timezone.datetime.now().date()) Reservation.objects.create(user=user, machine=Machine.objects.get(name="S1"), start_time=start_time, end_time=end_time, event=None) return Reservation.objects.first()
def test_to_python(self): machine_type_field = MachineTypeField() self.assertEqual(None, machine_type_field.to_python(None)) for machine_type in MachineTypeField.possible_machine_types: self.assertEqual(machine_type, machine_type_field.to_python(machine_type.id)) self.assertEqual(machine_type, machine_type_field.to_python(machine_type.id)) try: machine_type_field.to_python(0) self.fail("Invalid IDs should fail when converting to python") except ValidationError: pass try: machine_type_field.to_python(machine_type_field) self.fail("Invalid types should fail when converting to python") except ValidationError: pass
def test_get_admin_reservation(self): user = User.objects.create_user("test") machine_type = MachineTypeField.get_machine_type(1) Quota.objects.create(machine_type=machine_type, number_of_reservations=10, ignore_rules=True, user=user) permission = Permission.objects.get( codename="can_create_event_reservation") user.user_permissions.add(permission) event = Event.objects.create(title="Test_event") timeplace = TimePlace.objects.create( event=event, start_time=(timezone.now() + timedelta(hours=1)).time(), start_date=(timezone.now() + timedelta(hours=1)).date(), end_time=(timezone.now() + timedelta(hours=2)).time(), end_date=(timezone.now() + timedelta(hours=2)).date()) printer = Machine.objects.create(machine_type=machine_type, machine_model="Ultimaker") Printer3DCourse.objects.create(user=user, username=user.username, name=user.get_full_name(), date=timezone.now()) special_reservation = Reservation.objects.create( start_time=timezone.now() + timedelta(hours=1), special_text="Test", special=True, user=user, machine=printer, end_time=timezone.now() + timedelta(hours=2)) normal_reservation = Reservation.objects.create( start_time=timezone.now() + timedelta(hours=2), user=user, machine=printer, end_time=timezone.now() + timedelta(hours=3)) event_reservation = Reservation.objects.create( start_time=timezone.now() + timedelta(hours=3), event=timeplace, user=user, machine=printer, end_time=timezone.now() + timedelta(hours=4)) context_data = template_view_get_context_data(AdminReservationView, request_user=user) self.assertEqual(context_data["admin"], True) self.assertEqual(set(context_data["reservations"]), {special_reservation, event_reservation})
def test_is_valid_rule_external(self): ReservationRule(start_time=datetime.time(10, 0), end_time=datetime.time(6, 0), days_changed=1, start_days=53, max_inside_border_crossed=0, machine_type=self.machine_type, max_hours=0).save() rule = ReservationRule(start_time=datetime.time(12, 0), end_time=datetime.time(18, 0), days_changed=0, start_days=66, max_inside_border_crossed=0, machine_type=self.machine_type, max_hours=0) self.assertTrue(rule.is_valid_rule()) rule = ReservationRule(start_time=datetime.time(5, 0), end_time=datetime.time(12, 0), days_changed=0, start_days=2, max_inside_border_crossed=0, machine_type=self.machine_type, max_hours=0) self.assertFalse(rule.is_valid_rule()) try: rule.is_valid_rule(raise_error=True) self.fail( "An exception should have been raised for a check on a non valid rule with raise_error set" ) except ValidationError: pass try: rule.save() self.fail("Valid rules should not be saveable") except ValidationError: pass rule.machine_type = MachineTypeField.get_machine_type(2) self.assertTrue( rule.is_valid_rule(), "Rules for different machine types should not effect the validity of each other" )
def test_status(self): machine_type = MachineTypeField.get_machine_type(1) printer = Machine.objects.create(name="C1", location="Printer room", status="F", machine_model="Ultimaker 2 Extended", machine_type=machine_type) user = User.objects.create_user("test") Printer3DCourse.objects.create(name="Test", username="******", user=user, date=timezone.datetime.now().date()) Quota.objects.create(machine_type=machine_type, user=user, ignore_rules=True, number_of_reservations=1) self.assertEquals(printer.get_status(), "F") self.assertEquals(printer.get_status_display(), "Ledig") printer.status = "O" self.assertEquals(printer.get_status(), "O") self.assertEquals(printer.get_status_display(), "I ustand") printer.status = "M" self.assertEquals(printer.get_status(), "M") self.assertEquals(printer.get_status_display(), "Vedlikehold") printer.status = "R" self.assertEquals(printer.get_status(), "F") self.assertEquals(printer.get_status_display(), "Ledig") Reservation.objects.create(machine=printer, start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=1), user=user) self.assertEquals(printer.get_status(), "R") self.assertEquals(printer.get_status_display(), "Reservert") printer.status = "F" self.assertEquals(printer.get_status(), "R") self.assertEquals(printer.get_status_display(), "Reservert") printer.status = "O" self.assertEquals(printer.get_status(), "O") self.assertEquals(printer.get_status_display(), "I ustand") printer.status = "M" self.assertEquals(printer.get_status(), "M") self.assertEquals(printer.get_status_display(), "Vedlikehold")
def setUp(self): self.user = User.objects.create_user("test") self.machine_type = MachineTypeField.get_machine_type(2) self.machine = Machine.objects.create(machine_type=self.machine_type, status="F", name="Test") Quota.objects.create(machine_type=self.machine_type, number_of_reservations=2, ignore_rules=False, all=True) ReservationRule.objects.create(start_time=time(0, 0), end_time=time(23, 59), start_days=1, days_changed=6, max_inside_border_crossed=6, max_hours=6, machine_type=self.machine_type)
def setUp(self): Reservation.reservation_future_limit_days = 7 self.machine_type_sewing = MachineTypeField.get_machine_type(2) self.user = User.objects.create_user(username="******") self.machine = Machine.objects.create( machine_model="Test", machine_type=self.machine_type_sewing) self.event = Event.objects.create(title="Test_event") Quota.objects.create(user=self.user, machine_type=self.machine_type_sewing, number_of_reservations=100, ignore_rules=True) self.timeplace = TimePlace.objects.create( event=self.event, start_time=(timezone.now() + timedelta(hours=1)).time(), start_date=(timezone.now() + timedelta(hours=1)).date(), end_time=(timezone.now() + timedelta(hours=2)).time(), end_date=(timezone.now() + timedelta(hours=2)).date())
class Machine(models.Model): status_choices = ( ("R", _("Reserved")), ("F", _("Available")), ("I", _("In use")), ("O", _("Out of order")), ("M", _("Maintenance")), ) status = models.CharField(max_length=2, choices=status_choices) name = models.CharField(max_length=30, unique=True) location = models.CharField(max_length=40) location_url = models.URLField() machine_model = models.CharField(max_length=40) machine_type = MachineTypeField(null=True) @abstractmethod def get_reservation_set(self): return Reservation.objects.filter(machine=self) @abstractmethod def can_user_use(self, user): return self.machine_type.can_user_use(user) def reservations_in_period(self, start_time, end_time): return self.get_reservation_set().filter(start_time__lte=start_time, end_time__gt=start_time) | \ self.get_reservation_set().filter(start_time__gte=start_time, end_time__lte=end_time) | \ self.get_reservation_set().filter(start_time__lt=end_time, start_time__gt=start_time, end_time__gte=end_time) def __str__(self): return self.name + " - " + self.machine_model def get_status(self): if self.status in "OM": return self.status return self.reservations_in_period( timezone.now(), timezone.now() + timedelta(seconds=1)) and "R" or "F" def get_status_display(self): current_status = self.get_status() return [ full_name for short_hand, full_name in self.status_choices if short_hand == current_status ][0]
def test_get_user_quota(self): user = User.objects.create_user("test") user2 = User.objects.create_user("test2") machine_type = MachineTypeField.get_machine_type(1) Quota.objects.create(all=True, user=user, machine_type=machine_type, number_of_reservations=2) quota2 = Quota.objects.create(user=user, machine_type=machine_type, number_of_reservations=2) Quota.objects.create(user=user2, machine_type=machine_type, number_of_reservations=2) context_data = template_view_get_context_data(GetUserQuotaView, request_user=user, user=user) context_data["user_quotas"] = list(context_data["user_quotas"]) self.assertEqual(context_data, {"user_quotas": [quota2]})
def setUp(self): user1 = User.objects.create_user("user1", "*****@*****.**", "weak_pass") user1.save() user2 = User.objects.create_user("user2", "*****@*****.**", "weak_pass") user2.save() machine_type_printer = MachineTypeField.get_machine_type(1) Machine.objects.create(name="U1", location="Make", machine_model="Ultimaker 2", status="F", machine_type=machine_type_printer) Quota.objects.create(user=user1, number_of_reservations=2, ignore_rules=True, machine_type=machine_type_printer) Quota.objects.create(user=user2, number_of_reservations=2, ignore_rules=True, machine_type=machine_type_printer) Printer3DCourse.objects.create(user=user1, username=user1.username, name=user1.get_full_name(), date=timezone.now()) Printer3DCourse.objects.create(user=user2, username=user2.username, name=user2.get_full_name(), date=timezone.now()) self.reservation = Reservation.objects.create( user=user1, machine=Machine.objects.get(name="U1"), start_time=timezone.now() + timezone.timedelta(hours=2), end_time=timezone.now() + timezone.timedelta(hours=4), event=None)
def to_python(self, value): return MachineTypeField.get_machine_type(int(value))
def setUp(self): self.period = ReservationRule.Period self.machine_type = MachineTypeField.get_machine_type(1)
def setUp(self): self.machine_type = MachineTypeField.get_machine_type(1)
class ReservationRule(models.Model): start_time = models.TimeField(verbose_name=_("Start time")) end_time = models.TimeField(verbose_name=_("End time")) # Number of times passed by midnight between start and end time days_changed = models.IntegerField(verbose_name=_("Days")) start_days = models.IntegerField(default=0, verbose_name=_("Start days")) max_hours = models.FloatField(verbose_name=_("Hours single period")) max_inside_border_crossed = models.FloatField( verbose_name=_("Hours multiperiod")) machine_type = MachineTypeField(verbose_name=_("Machine type")) def save(self, **kwargs): if not self.is_valid_rule(): raise ValidationError("Rule is not valid") return super().save(**kwargs) @staticmethod def valid_time(start_time, end_time, machine_type): # Normal non rule ignoring reservations will not be longer than 1 week if timedelta_to_hours(end_time - start_time) > 168: return False rules = [ rule for rule in ReservationRule.objects.filter( machine_type=machine_type) if rule.hours_inside(start_time, end_time) ] if timedelta_to_hours(end_time - start_time) > max(rule.max_hours for rule in rules): return False if timedelta_to_hours(end_time - start_time) <= min(rule.max_hours for rule in rules): return True return all( rule.valid_time_in_rule(start_time, end_time, len(rules) > 1) for rule in rules) class Period: def __init__(self, start_day, rule): self.start_time = self.__to_inner_rep(start_day, rule.start_time) self.end_time = self.__to_inner_rep(start_day + rule.days_changed, rule.end_time) self.rule = rule def hours_inside(self, start_time, end_time): return self.hours_overlap( self.start_time, self.end_time, self.__to_inner_rep(start_time.weekday(), start_time.time()), self.__to_inner_rep(end_time.weekday(), end_time.time())) @staticmethod def hours_overlap(a, b, c, d): b, c, d = (b - a) % 7, (c - a) % 7, (d - a) % 7 if c > d: return min(b, d) * 24 return (min(b, d) - min(b, c)) * 24 @staticmethod def __to_inner_rep(day, time): return day + time.hour / 24 + time.minute / 1440 + time.second / 86400 def overlap(self, other): return self.hours_overlap(self.start_time, self.end_time, other.start_time, other.end_time) > 0 def is_valid_rule(self, raise_error=False): # Check if the time period is a valid time period (within a week) if self.start_time > self.end_time and not self.days_changed or self.days_changed > 7 or \ self.days_changed == 7 and self.start_time < self.end_time: if raise_error: raise ValidationError( "Period is either too long (7+ days) or start time is earlier than end time." ) return False # Check for internal overlap time_periods = self.time_periods() if any( t1.overlap(t2) for t1 in time_periods for t2 in time_periods if t1.end_time != t2.end_time and t1.start_time != t2.end_time): if raise_error: raise ValidationError( "Rule has internal overlap of time periods.") return False # Check for overlap with other time periods other_time_periods = [ time_period for rule in ReservationRule.objects.filter( machine_type=self.machine_type).exclude(pk=self.pk) for time_period in rule.time_periods() ] other_overlap = any( t1.overlap(t2) for t1 in time_periods for t2 in other_time_periods) if raise_error and other_overlap: raise ValidationError( "Rule time periods overlap with time periods of other rules.") return not other_overlap def valid_time_in_rule(self, start_time, end_time, border_cross): if border_cross: return self.hours_inside( start_time, end_time) <= self.max_inside_border_crossed return timedelta_to_hours(end_time - start_time) <= self.max_hours def hours_inside(self, start_time, end_time): return sum( period.hours_inside(start_time, end_time) for period in self.time_periods()) def time_periods(self): return [ self.Period(day_index, self) for day_index, _ in filter( lambda enumerate_obj: enumerate_obj[1] == "1", enumerate(format(self.start_days, "07b")[::-1])) ]
class Quota(models.Model): number_of_reservations = models.IntegerField( default=1, verbose_name=_("Number of reservations")) diminishing = models.BooleanField(default=False, verbose_name=_("Diminishing")) ignore_rules = models.BooleanField(default=False, verbose_name=_("Ignores rules")) all = models.BooleanField(default=False, verbose_name=_("All users")) user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name=_("User")) machine_type = MachineTypeField(null=True, verbose_name=_("Machine type")) class Meta: permissions = ( ("can_create_event_reservation", "Can create event reservation"), ("can_edit_quota", "Can edit quotas"), ) def get_active_reservations(self, user): if self.diminishing: return self.reservation_set.all() reservation_set = self.reservation_set if not self.all else self.reservation_set.filter( user=user) return reservation_set.all().filter(end_time__gte=timezone.now()) def can_make_more_reservations(self, user): return self.number_of_reservations != self.get_active_reservations( user).count() def is_valid_in(self, reservation): return (self.reservation_set.filter(pk=reservation.pk).exists() or self.can_make_more_reservations(reservation.user)) and \ (self.ignore_rules or ReservationRule.valid_time(reservation.start_time, reservation.end_time, reservation.machine.machine_type)) @staticmethod def can_make_new_reservation(user, machine_type): return any( quota.can_make_more_reservations(user) for quota in Quota.get_user_quotas(user, machine_type)) @staticmethod def get_user_quotas(user, machine_type): return Quota.objects.filter(Q(user=user) | Q(all=True)).filter( machine_type=machine_type) @staticmethod def get_best_quota(reservation): """ Selects the best quota for the given reservation, by preferring non-diminishing quotas that do not ignore the rules :param reservation: The reservation to check :return: The best quota, that can handle the given reservation, or None if none can """ valid_quotas = [ quota for quota in Quota.get_user_quotas( reservation.user, reservation.machine.machine_type) if quota.is_valid_in(reservation) ] if not valid_quotas: return None best_quota = valid_quotas[0] for quota in valid_quotas[1:]: if best_quota.diminishing: if not quota.diminishing or best_quota.ignore_rules and not quota.ignore_rules: best_quota = quota elif best_quota.ignore_rules and not quota.ignore_rules: best_quota = quota return best_quota @staticmethod def can_make_reservation(reservation): return Quota.get_best_quota(reservation) is not None