Skip to content

Commit

Permalink
Feature: Add maintenance mode (#281)
Browse files Browse the repository at this point in the history
* Add maintenance mode

* Create maintenance app

* Add user perm test during maintenance mode
  • Loading branch information
ezkat authored Sep 5, 2023
1 parent 6c6969a commit a59509f
Show file tree
Hide file tree
Showing 25 changed files with 354 additions and 82 deletions.
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

0 comments on commit a59509f

Please sign in to comment.