Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add maintenance mode #281

Merged
merged 4 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added maintenance/__init__.py
Empty file.
55 changes: 55 additions & 0 deletions maintenance/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django import forms
from django.contrib import admin
from django.db.models import Q
from django.contrib.admin import site as admin_site
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

from modeltranslation.admin import TranslationAdmin


from .models import MaintenanceMessage, MaintenanceMode


class MaintenanceModeAdmin(admin.ModelAdmin):
pass

class MaintenanceModeInline(admin.TabularInline):
model = MaintenanceMode
fields = ('start', 'end', )
verbose_name = _('maintenance mode')
verbose_name_plural = _('maintenance modes')
extra = 0


class MaintenanceMessageAdminForm(forms.ModelForm):
class Meta:
model = MaintenanceMessage
fields = ('start', 'end', 'message', )

def clean(self):
start = self.cleaned_data['start']
end = self.cleaned_data['end']
query = Q(end__gt=start, start__lt=end)
if self.instance and self.instance.pk:
query &= ~Q(pk=self.instance.pk)
collision = MaintenanceMessage.objects.filter(query)
if collision.exists():
raise ValidationError(_('maintenance message already exists.'))

class MaintenanceMessageAdmin(TranslationAdmin):
form = MaintenanceMessageAdminForm
inlines = ( MaintenanceModeInline, )
fieldsets = (
(_('General'), {
'fields': (
'start',
'end',
'message'
),
}),
)


admin_site.register(MaintenanceMessage, MaintenanceMessageAdmin)
admin_site.register(MaintenanceMode, MaintenanceModeAdmin)
1 change: 1 addition & 0 deletions maintenance/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .announcements import MaintenanceMessageViewSet
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework import viewsets
from .base import TranslatedModelSerializer, register_view
from resources.models import MaintenanceMessage
from resources.api.base import TranslatedModelSerializer, register_view
from maintenance.models import MaintenanceMessage



Expand Down
6 changes: 6 additions & 0 deletions maintenance/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class MaintenanceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'maintenance'
57 changes: 57 additions & 0 deletions maintenance/migrations/0001_create_maintenance_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generated by Django 3.2.19 on 2023-09-04 06:19

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='MaintenanceMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Time of creation')),
('modified_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Time of modification')),
('message', models.TextField(verbose_name='Message')),
('message_fi', models.TextField(null=True, verbose_name='Message')),
('message_en', models.TextField(null=True, verbose_name='Message')),
('message_sv', models.TextField(null=True, verbose_name='Message')),
('start', models.DateTimeField(verbose_name='Begin time')),
('end', models.DateTimeField(verbose_name='End time')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='maintenancemessage_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='maintenancemessage_modified', to=settings.AUTH_USER_MODEL, verbose_name='Modified by')),
],
options={
'verbose_name': 'maintenance message',
'verbose_name_plural': 'maintenance messages',
'ordering': ('start',),
},
),
migrations.CreateModel(
name='MaintenanceMode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Time of creation')),
('modified_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Time of modification')),
('start', models.DateTimeField(verbose_name='Begin time')),
('end', models.DateTimeField(verbose_name='End time')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='maintenancemode_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
('maintenance_message', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='maintenance.maintenancemessage')),
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='maintenancemode_modified', to=settings.AUTH_USER_MODEL, verbose_name='Modified by')),
],
options={
'verbose_name': 'maintenance mode',
'verbose_name_plural': 'maintenance modes',
'ordering': ('start',),
},
),
]
Empty file.
62 changes: 62 additions & 0 deletions maintenance/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import datetime


from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError


from resources.models.base import ModifiableModel


class MaintenanceMessageQuerySet(models.QuerySet):
def active(self):
return self.filter(start__lt=timezone.now(), end__gt=timezone.now())

class MaintenanceMessage(ModifiableModel):
message = models.TextField(verbose_name=_('Message'), null=False, blank=False)
start = models.DateTimeField(verbose_name=_('Begin time'), null=False, blank=False)
end = models.DateTimeField(verbose_name=_('End time'), null=False, blank=False)


objects = MaintenanceMessageQuerySet.as_manager()
class Meta:
verbose_name = _('maintenance message')
verbose_name_plural = _('maintenance messages')
ordering = ('start', )


def __str__(self):
return f"{_('maintenance message')} \
{timezone.localtime(self.start).replace(tzinfo=None)} - \
{timezone.localtime(self.end).replace(tzinfo=None)}" \
.capitalize()


def clean(self):
super().clean()
if self.end <= self.start:
raise ValidationError(_("Invalid start or end time"))

class MaintenanceModeQuerySet(models.QuerySet):
def active(self):
return self.filter(start__lt=timezone.now(), end__gt=timezone.now())


class MaintenanceMode(ModifiableModel):
start = models.DateTimeField(verbose_name=_('Begin time'), null=False, blank=False)
end = models.DateTimeField(verbose_name=_('End time'), null=False, blank=False)
maintenance_message = models.ForeignKey(MaintenanceMessage, on_delete=models.CASCADE, null=True, blank=True)

objects = MaintenanceModeQuerySet.as_manager()

class Meta:
verbose_name = _('maintenance mode')
verbose_name_plural = _('maintenance modes')
ordering = ('start', )

def clean(self):
super().clean()
if self.end <= self.start:
raise ValidationError(_("Invalid start or end time"))
3 changes: 3 additions & 0 deletions maintenance/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
8 changes: 8 additions & 0 deletions maintenance/translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from .models import MaintenanceMessage


@register(MaintenanceMessage)
class MaintenanceMessageTranslationOptions(TranslationOptions):
fields = ('message', )
required_languages = ('fi', 'en', 'sv', )
3 changes: 3 additions & 0 deletions maintenance/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.shortcuts import render

# Create your views here.
30 changes: 2 additions & 28 deletions resources/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.contrib.admin import site as admin_site
from django.contrib.admin.utils import unquote
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.admin.options import InlineModelAdmin
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.gis.admin import OSMGeoAdmin
Expand All @@ -29,7 +30,7 @@
ReservationHomeMunicipalityField, ReservationHomeMunicipalitySet, Resource, ResourceTag, ResourceAccessibility,
ResourceEquipment, ResourceGroup, ResourceImage, ResourceType, TermsOfUse,
Unit, UnitAuthorization, UnitIdentifier, UnitGroup, UnitGroupAuthorization,
MaintenanceMessage, UniversalFormFieldType, ResourceUniversalField, ResourceUniversalFormOption,
UniversalFormFieldType, ResourceUniversalField, ResourceUniversalFormOption
)
from ..models.utils import generate_id
from munigeo.models import Municipality
Expand Down Expand Up @@ -529,32 +530,6 @@ class RespaTokenAdmin(admin.ModelAdmin):
raw_id_fields = ('user',)


class MaintenanceMessageAdminForm(forms.ModelForm):
class Meta:
model = MaintenanceMessage
fields = ('start', 'end', 'message', )

def clean(self):
start = self.cleaned_data['start']
end = self.cleaned_data['end']
query = Q(end__gt=start, start__lt=end)
if self.instance and self.instance.pk:
query &= ~Q(pk=self.instance.pk)
collision = MaintenanceMessage.objects.filter(query)
if collision.exists():
raise ValidationError(_('maintenance message already exists.'))

class MaintenanceMessageAdmin(TranslationAdmin):
form = MaintenanceMessageAdminForm
fieldsets = (
(_('General'), {
'fields': (
'start',
'end',
'message'
),
}),
)

admin_site.register(ResourceImage, ResourceImageAdmin)
admin_site.register(Resource, ResourceAdmin)
Expand Down Expand Up @@ -584,6 +559,5 @@ class MaintenanceMessageAdmin(TranslationAdmin):
if admin.site.is_registered(Token):
admin.site.unregister(Token)
admin_site.register(Token, RespaTokenAdmin)
admin_site.register(MaintenanceMessage, MaintenanceMessageAdmin)
admin_site.register(UniversalFormFieldType, UniversalFieldAdmin)
admin_site.register(ResourceUniversalFormOption, ResourceUniversalFormOptionAdmin)
1 change: 0 additions & 1 deletion resources/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from .unit import UnitViewSet
from .search import TypeaheadViewSet
from .equipment import EquipmentViewSet
from .announcements import MaintenanceMessageViewSet

from rest_framework import routers

Expand Down
7 changes: 7 additions & 0 deletions resources/api/reservation.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
from respa.renderers import ResourcesBrowsableAPIRenderer
from payments.utils import is_free, get_price

from maintenance.models import MaintenanceMode

User = get_user_model()

# FIXME: Make this configurable?
Expand Down Expand Up @@ -266,6 +268,11 @@ def validate(self, data):

obj_user_is_staff = bool(request_user and request_user.is_staff)

if (not reservation or (reservation and reservation.state != Reservation.WAITING_FOR_PAYMENT)) \
and MaintenanceMode.objects.active().exists():
raise ValidationError(_('Reservations are disabled at this moment.'))


try:
resource = data['resource']
except KeyError:
Expand Down
16 changes: 15 additions & 1 deletion resources/api/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from rest_framework.settings import api_settings as drf_settings
from rest_framework.relations import PrimaryKeyRelatedField
from resources.models.utils import log_entry
from maintenance.models import MaintenanceMode

from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
Expand Down Expand Up @@ -615,6 +616,7 @@ class ResourceSerializer(ExtraDataMixin, TranslatedModelSerializer, munigeo_api.
resource_staff_emails = ResourceStaffEmailsField()
universal_field = ResourceUniversalFieldSerializer(many=True, read_only=True, source='resource_universal_field')
reservable_by_all_staff = serializers.BooleanField(required=False)
reservable = serializers.SerializerMethodField()

def get_max_price_per_hour(self, obj):
"""Backwards compatibility for 'max_price_per_hour' field that is now deprecated"""
Expand Down Expand Up @@ -662,7 +664,8 @@ def get_user_permissions(self, obj):
user = prefetched_user or request.user

can_make_reservations_for_customers = obj.can_create_reservations_for_other_users(user) if request else False
return {

permissions = {
'can_make_reservations': obj.can_make_reservations(user) if request else False,
**({'can_make_reservations_for_customer': can_make_reservations_for_customers} if (request and can_make_reservations_for_customers) else {}),
'can_ignore_opening_hours': obj.can_ignore_opening_hours(user) if request else False,
Expand All @@ -672,6 +675,12 @@ def get_user_permissions(self, obj):
'can_bypass_payment': obj.can_bypass_payment(user) if request else False,
}


if MaintenanceMode.objects.active().exists():
return permissions.fromkeys(permissions, False)

return permissions

def get_is_favorite(self, obj):
request = self.context.get('request', None)
return request.user in obj.favorited_by.all()
Expand All @@ -684,6 +693,11 @@ def get_payment_terms(self, obj):
data = TermsOfUseSerializer(obj.payment_terms).data
return data['text']

def get_reservable(self, obj):
if MaintenanceMode.objects.active().exists():
return False
return obj.reservable

def get_reservable_before(self, obj):
request = self.context.get('request')
prefetched_user = self.context.get('prefetched_user', None)
Expand Down
35 changes: 35 additions & 0 deletions resources/migrations/0148_add_maintenance_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 3.2.19 on 2023-09-01 09:35

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('resources', '0147_reserver_id_only_business'),
]

operations = [
migrations.CreateModel(
name='MaintenanceMode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Time of creation')),
('modified_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Time of modification')),
('start', models.DateTimeField(verbose_name='Begin time')),
('end', models.DateTimeField(verbose_name='End time')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='maintenancemode_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
('maintenance_message', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='resources.maintenancemessage')),
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='maintenancemode_modified', to=settings.AUTH_USER_MODEL, verbose_name='Modified by')),
],
options={
'verbose_name': 'maintenance mode',
'verbose_name_plural': 'maintenance modes',
'ordering': ('start',),
},
),
]
Loading