Commit 82b7cb7b authored by Bryan  BRANCOTTE's avatar Bryan BRANCOTTE

Merge branch 'live-settings' into 'master'

Live settings

Closes #147

See merge request !31
parents d86b61fe d8ff1df5
......@@ -11,7 +11,7 @@ variables:
build:
stage: build
variables:
RUN_TEST: "0"
RUN_TEST: "1"
script:
- if [ $CI_COMMIT_REF_SLUG == "master" ]; then export RUN_TEST="1"; fi
# - export
......
from django.core.cache import cache
from django.db import OperationalError
class LiveSettings(object):
def __setattr__(self, key, value, *args, **kwargs):
try:
from live_settings.models import LiveSettings
LiveSettings.objects.update_or_create(key=key, defaults=dict(value=value))
except OperationalError:
return super().__setattr__(key, value)
def __getattribute__(self, key):
live_settings_dict = cache.get("live_settings_dict")
if live_settings_dict is None:
try:
from live_settings.models import LiveSettings
live_settings_dict = dict(
LiveSettings.objects.values_list("key", "value")
)
cache.set("live_settings_dict", live_settings_dict)
except OperationalError:
return super().__getattribute__(key)
return live_settings_dict.get(key, None)
live_settings = LiveSettings()
from django.contrib import admin
from live_settings import models
class LiveSettingsAdmin(admin.ModelAdmin):
list_display = ("key", "last_edition_date")
readonly_fields = ["last_edition_date"]
admin.site.register(models.LiveSettings, LiveSettingsAdmin)
from live_settings import live_settings
def processors(request):
return dict(live_settings=live_settings)
from django import forms
from live_settings import models
class LiveSettingsForm(forms.ModelForm):
next = forms.CharField(widget=forms.HiddenInput(), required=True)
class Meta:
model = models.LiveSettings
fields = ("value",)
widgets = {"value": forms.HiddenInput()}
# Generated by Django 2.2.5 on 2019-11-20 10:20
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='LiveSettings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=256, unique=True, validators=[django.core.validators.RegexValidator(code='invalid_live_settings_key', message='LiveSettings key must must be Alphanumeric and start with a letter: <code>^[a-zA-Z][\\w]*$</code>.', regex='^[a-zA-Z][\\w]*$')])),
('value', models.TextField(blank=True, null=True)),
('last_edition_date', models.DateTimeField()),
],
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.auth import get_permission_codename
from django.contrib.auth.models import Permission, Group
from django.contrib.contenttypes.models import ContentType
from django.db import migrations
def migration_code(apps, schema_editor):
# inspired by django.contrib.auth.management.create_permissions
group, _ = Group.objects.get_or_create(name="LiveSettingEditor")
for klass in [
apps.get_model("live_settings", "LiveSettings"),
]:
ct = ContentType.objects.get_for_model(klass)
opts = klass._meta
for action in klass._meta.default_permissions:
perm, _ = Permission.objects.get_or_create(
codename=get_permission_codename(action, opts),
name='Can %s %s' % (action, opts.verbose_name_raw),
content_type=ct)
group.permissions.add(perm)
def reverse_code(apps, schema_editor):
Group.objects.filter(name="LiveSettingEditor").delete()
class Migration(migrations.Migration):
dependencies = [
('live_settings', '0001_initial'),
]
operations = [
migrations.RunPython(migration_code, reverse_code=reverse_code),
]
from __future__ import unicode_literals
from django.core.cache import cache
from django.core.validators import RegexValidator
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.utils import timezone
from django.utils.safestring import mark_safe
class LiveSettings(models.Model):
class Meta:
verbose_name = "Live setting"
verbose_name_plural = "Live settings"
regex = "^[a-zA-Z][\\w]*$"
key = models.CharField(
max_length=256,
unique=True,
validators=(
RegexValidator(
regex=regex,
message=mark_safe(
"LiveSettings key must must be Alphanumeric and start with a letter: "
"<code>%s</code>." % regex
),
code="invalid_live_settings_key",
),
),
)
value = models.TextField(blank=True, null=True, )
last_edition_date = models.DateTimeField()
def save(self, *args, **kwargs):
self.last_edition_date = timezone.now()
super().save(*args, **kwargs)
@receiver(post_save, sender=LiveSettings)
@receiver(post_delete, sender=LiveSettings)
def flush_live_settings_in_cache(*args, **kwargs):
cache.delete("live_settings_dict")
{% extends "admin/index.html" %}
{% load i18n static basetheme_bootstrap %}
{% block extrastyle %}
{{ block.super }}
<style>
#content-related{
background: none;
}
#content-related .module{
background: #f8f8f8;
}
#site-wide-commands form{
margin:15px 7px;
}
</style>
{% endblock %}
{% block sidebar %}
<div id="content-related">
{% if perms.live_settings.change_livesettings %}
<div class="module" id="site-wide-commands">
<h2>{% trans 'Site wide settings' %}</h2>
{%include_if_exists "live_settings/sitewidecommands.html" %}
</div>
{%endif%}
<div class="module" id="recent-actions-module">
<h2>{% trans 'Recent actions' %}</h2>
<h3>{% trans 'My actions' %}</h3>
{% load log %}
{% get_admin_log 10 as admin_log for_user user %}
{% if not admin_log %}
<p>{% trans 'None available' %}</p>
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
{% if entry.is_deletion or not entry.get_admin_url %}
{{ entry.object_repr }}
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
<br>
{% if entry.content_type %}
<span class="mini quiet">{% filter capfirst %}{{ entry.content_type }}{% endfilter %}</span>
{% else %}
<span class="mini quiet">{% trans 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<hr/>
</div>
</div>
{% endblock %}
\ No newline at end of file
import logging
from unittest import TestCase
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core import management
from django.core.cache import cache
from django.test import TestCase as DjangoTestCase
from django.urls import reverse
from live_settings import live_settings, models
logger = logging.getLogger(__name__)
class LiveSettingsTestCase(DjangoTestCase):
def setUp(self) -> None:
super().setUp()
################################################################################
self.user = get_user_model().objects.create(
username="root",
)
def test_get_value(self):
self.assertIsNone(live_settings.toto)
models.LiveSettings.objects.create(key="toto", value="tata")
self.assertIsNotNone(live_settings.toto)
self.assertEqual(live_settings.toto, "tata")
def test_get_set(self):
live_settings.tralala = 1
self.assertEqual(str(live_settings.tralala), "1")
def test_view_works(self):
form_data = dict(value="titi", next="/")
url = reverse('live_settings:update', args=["toto"])
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(live_settings.toto, form_data["value"])
self.client.force_login(self.user)
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(live_settings.toto, form_data["value"])
change = Permission.objects.get(
content_type=ContentType.objects.get_for_model(models.LiveSettings),
codename__startswith="change")
self.user.user_permissions.add(change)
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
self.assertNotEqual(live_settings.toto, form_data["value"])
response = self.client.post(url, dict(value=form_data["value"]))
self.assertEqual(response.status_code, 400)
self.assertNotEqual(live_settings.toto, form_data["value"])
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 302)
self.assertEqual(live_settings.toto, form_data["value"])
class LiveSettingsNoDBTestCase(TestCase):
def test_get_value(self):
management.call_command("migrate", "live_settings", "zero", no_input=True, skip_checks=True)
cache.delete("live_settings_dict")
try:
ex = live_settings.tralala
except Exception as e:
ex = e
assert isinstance(ex, AttributeError)
live_settings.tralala = 1
from django.conf.urls import url
from live_settings import views
app_name = "live_settings"
urlpatterns = [
url(r"^update/(?P<slug>[a-zA-Z][\w]*)/$", views.change_value, name="update")
]
from django.contrib.auth.decorators import permission_required
from django.http import HttpResponseForbidden, HttpResponseBadRequest
from django.shortcuts import redirect
from live_settings import forms
from live_settings import live_settings
@permission_required("live_settings.change_livesettings")
def change_value(request, slug):
if request.method != "POST":
return HttpResponseForbidden()
form = forms.LiveSettingsForm(data=request.POST)
if not form.is_valid():
return HttpResponseBadRequest()
setattr(live_settings, slug, form.cleaned_data["value"])
return redirect(form.cleaned_data["next"])
......@@ -36,6 +36,7 @@ ALLOWED_HOSTS = [s.strip() for s in config('ALLOWED_HOSTS').split(',')] \
# Application definition
INSTALLED_APPS = [
'live_settings',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
......@@ -77,6 +78,7 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'basetheme_bootstrap.context_processors.processors',
"live_settings.context_processors.processors",
],
},
},
......
......@@ -28,6 +28,7 @@ urlpatterns = [
# path('api/', include('webapi.urls')),
path('admin/doc/', include('django.contrib.admindocs.urls')),
path('admin/', admin.site.urls),
url(r"^settings/", include("live_settings.urls")),
url(r'^$', views.index, name='home'),
]
......
# Generated by Django 2.2.5 on 2019-11-19 10:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('viralhostrangedb', '0029_auto_20191029_1028'),
]
operations = [
migrations.AlterIndexTogether(
name='globalviralhostresponsevalue',
index_together={('id', 'name'), ('id', 'name', 'value'), ('id', 'value')},
),
]
{% load i18n static %}
<form method="POST" action="{% url 'live_settings:update' slug='enable_meta_analysis' %}"
style="display:block">{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}"/>
<div>
<input type="checkbox" id="metaanalysis" name="value" value="True"
onchange="this.closest('form').submit()"
{% if live_settings.enable_meta_analysis == 'True' %}checked="checked"{%endif%}
>
<label for="metaanalysis">Allow meta analysis</label>
</div>
</form>
<hr/>
<form method="POST" action="{% url 'live_settings:update' slug='number_of_recent_data_source' %}"
style="display:block">{% csrf_token %}
<label for="number_of_recent_data_source">Number of recent data source rendered</label><br/>
<input type="hidden" name="next" value="{{ request.path }}"/>
<input type="number" id="number_of_recent_data_source" name="value" value="{{live_settings.number_of_recent_data_source}}"
onchange="this.closest('form').submit()"
>
</form>
<hr/>
\ No newline at end of file
......@@ -61,6 +61,7 @@
</div>
</div>
{% if live_settings.number_of_recent_data_source != "0" %}
<div class="col-12 col-lg-10 offset-lg-1 col-xl-8 offset-xl-2">
<h1 class="text-center mt-4 pt-4">{% trans "title_subset_of_data_source"%}</h1>
<div class="row">
......@@ -94,6 +95,7 @@
{%endfor%}
</div>
</div>
{%endif%}
<div class="col-12 col-lg-10 offset-lg-1 col-xl-8 offset-xl-2">
<h1 class="text-center mt-4 pt-4">{% trans "access_to_virus_host_and_data_source"%}</h1>
<div class="row homepage-lower">
......@@ -121,6 +123,7 @@
<div class="pb-4">{% trans "Open the full list"%}</div>
</a>
</div>
{%if live_settings.enable_meta_analysis == "True" %}
<div class="col-12 col-md-4 offset-md-2">
<a href="{% url 'viralhostrangedb:download_responses' %}?user_pref=False&only_host_ncbi_id=true&only_virus_ncbi_id=true&allow_overflow=on" role="button"
class="homepage-lower-button bg-primary">
......@@ -137,6 +140,7 @@
<div class="pb-4 pl-1 pr-1">{% trans "Explore all responses on NCBI identified viruses and hosts"%}</div>
</a>
</div>
{%endif%}
</div>
</div>
{% endblock content%}
\ No newline at end of file
......@@ -12,6 +12,7 @@ from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from live_settings import live_settings
from viralhostrangedb import models, business_process
logger = logging.getLogger(__name__)
......@@ -377,9 +378,16 @@ class ViewTestCase(TestCase):
class HomePageTestCase(ViewTestCase):
def test_works(self):
self.assertTrue(self.client.login(username="user", email="a@a.a", password=self.user_pwd))
response = self.client.get(reverse('viralhostrangedb:home'))
self.assertEqual(response.status_code, 200)
for cpt in [1, 3]:
live_settings.number_of_recent_data_source = cpt
response = self.client.get(reverse('viralhostrangedb:home'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(str(response.content).split("card-data-source")), cpt + 1)
class FileImportTestCase(ViewTestCase):
def test_works(self):
......
......@@ -32,6 +32,7 @@ from django.views.decorators.cache import cache_page
from django.views.generic import ListView, DetailView, UpdateView, DeleteView
from formtools.wizard import views as wizard_views
from live_settings import live_settings
from viralhostrangedb import forms, business_process, views_api
from viralhostrangedb import mixins
from viralhostrangedb import models
......@@ -45,7 +46,13 @@ def index(request):
request=request,
queryset=models.DataSource.objects_annotated_with_pending.filter(mapping_done=True),
).filter(Q(public=True) | ~Q(description=""))
top_data_sources = data_sources.order_by("-last_edition_date")[0:4]
try:
qte = int(live_settings.number_of_recent_data_source)
except ValueError:
qte = 4
except TypeError:
qte = 4
top_data_sources = data_sources.order_by("-last_edition_date")[0:qte]
context = dict(
n=range(3),
data_sources=data_sources,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment