Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
14 changes: 8 additions & 6 deletions admin/maintenance/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytz
import datetime

from osf.models import MaintenanceState
from osf.models import MaintenanceState, MaintenanceMode
import website.maintenance as maintenance
from admin.maintenance.forms import MaintenanceForm

Expand Down Expand Up @@ -36,15 +36,17 @@ def get_context_data(self, **kwargs):
maintenance = MaintenanceState.objects.first()
kwargs['form'] = MaintenanceForm()
kwargs['current_alert'] = model_to_dict(maintenance) if maintenance else None
kwargs['maintenance_mode'] = MaintenanceMode.is_under_maintenance()
return super().get_context_data(**kwargs)

def post(self, request, *args, **kwargs):
data = request.POST

start = convert_eastern_to_utc(data['start']).isoformat() if data.get('start') else None
end = convert_eastern_to_utc(data['end']).isoformat() if data.get('end') else None

maintenance.set_maintenance(data.get('message', ''), data['level'], start, end)
if maintenance_mode := data.get('maintenance_mode'):
MaintenanceMode(maintenance_mode=False if maintenance_mode == 'True' else True).save()
else:
start = convert_eastern_to_utc(data['start']).isoformat() if data.get('start') else None
end = convert_eastern_to_utc(data['end']).isoformat() if data.get('end') else None
maintenance.set_maintenance(data.get('message', ''), data['level'], start, end)
return redirect('maintenance:display')


Expand Down
15 changes: 15 additions & 0 deletions admin/templates/maintenance/display.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ <h4>Put up an alert:</h4>
</form>
</div>
</div>


<div class="row">
<div class="col-md-9" style="margin-top: 15px;">
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="maintenance_mode" value={{maintenance_mode}}>
{% if maintenance_mode %}
<input class="btn btn-success" type="submit" value="Turn off Maintenance Mode" />
{% else %}
<input class="btn btn-danger" type="submit" value="Turn on Maintenance Mode" />
{% endif %}
</form>
</div>
</div>
{% endif %}

{% endblock content %}
Expand Down
21 changes: 21 additions & 0 deletions api/base/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from importlib import import_module

from django.conf import settings
from django.http import JsonResponse
from django.contrib.sessions.middleware import SessionMiddleware
from django.utils.deprecation import MiddlewareMixin
from sentry_sdk import init
Expand All @@ -24,6 +25,7 @@
from .api_globals import api_globals
from api.base import settings as api_settings
from api.base.authentication.drf import drf_get_session_from_cookie
from osf.models import MaintenanceMode

SessionStore = import_module(settings.SESSION_ENGINE).SessionStore

Expand Down Expand Up @@ -132,3 +134,22 @@ def process_request(self, request):
request.session = drf_get_session_from_cookie(cookie)
else:
request.session = SessionStore()


class MaintenanceModeMiddleware:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super-familiar with Middleware in Django. Will this also prevent incoming requests from being processed (i.e. if someone POSTs or PATCHes something, will we prevent that from affecting the OSF DB)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, middleware calls for all requests and workflow goes to view only if MaintenanceMode.is_under_maintenance() is False

image

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
if request.path.endswith(('/v2', '/v2/')):
return self.get_response(request)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why you did this, based on the ticket, but I don't think this is explicitly necessary. Having the /v2 route return just the information from the maintenance mode section below would be fine.

if MaintenanceMode.is_under_maintenance():
return JsonResponse(
{
'meta': {
'maintenance_mode': True,
'status_page': 'status',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is supposed to be https://status.cos.io.

},
}, status=503,
)
return self.get_response(request)
1 change: 1 addition & 0 deletions api/base/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'api.base.middleware.UnsignCookieSessionMiddleware',
'api.base.middleware.MaintenanceModeMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
Expand Down
38 changes: 38 additions & 0 deletions osf/migrations/0039_maintenancemode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 4.2.26 on 2026-04-23 14:25

from django.db import migrations, models


def create_initial_record(apps, schema_editor):
MaintenanceMode = apps.get_model('osf', 'MaintenanceMode')
MaintenanceMode.objects.get_or_create(
pk=1,
defaults={'maintenance_mode': False}
)


def reverse_initial_record(apps, schema_editor):
# the reverse 'reverse_initial_record' does nothing
# because the table will be removed
pass


class Migration(migrations.Migration):

dependencies = [
('osf', '0038_abstractnode_date_last_indexed_and_more'),
]

operations = [
migrations.CreateModel(
name='MaintenanceMode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('maintenance_mode', models.BooleanField(default=False)),
],
),
migrations.RunPython(
create_initial_record,
reverse_code=reverse_initial_record
),
]
2 changes: 1 addition & 1 deletion osf/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from .institution_affiliation import InstitutionAffiliation
from .institution_storage_region import InstitutionStorageRegion
from .licenses import NodeLicense, NodeLicenseRecord
from .maintenance_state import MaintenanceState
from .maintenance_state import MaintenanceState, MaintenanceMode
from .metadata import GuidMetadataRecord
from .metaschema import (
FileMetadataSchema,
Expand Down
13 changes: 13 additions & 0 deletions osf/models/maintenance_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,16 @@ class MaintenanceState(models.Model):
start = NonNaiveDateTimeField()
end = NonNaiveDateTimeField()
message = models.TextField(blank=True)


class MaintenanceMode(models.Model):
maintenance_mode = models.BooleanField(default=False)

def save(self, *args, **kwargs):
self.pk = 1
super().save(*args, **kwargs)

@classmethod
def is_under_maintenance(cls):
obj, _ = cls.objects.get_or_create(pk=1)
return obj.maintenance_mode
Loading