mirror of
https://github.com/caronc/apprise-api.git
synced 2025-08-18 18:38:10 +02:00
APPRISE_CONFIG_LOCK switch added for extra security (#57)
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
from .utils import ConfigCache
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def stateful_mode(request):
|
||||
@@ -30,3 +31,10 @@ def stateful_mode(request):
|
||||
Returns our loaded Stateful Mode
|
||||
"""
|
||||
return {'STATEFUL_MODE': ConfigCache.mode}
|
||||
|
||||
|
||||
def config_lock(request):
|
||||
"""
|
||||
Returns the state of our global configuration lock
|
||||
"""
|
||||
return {'CONFIG_LOCK': settings.APPRISE_CONFIG_LOCK}
|
||||
|
@@ -90,6 +90,7 @@ class AddByConfigForm(forms.Form):
|
||||
label=_('Configuration'),
|
||||
widget=forms.Textarea(),
|
||||
max_length=4096,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def clean_format(self):
|
||||
|
@@ -8,13 +8,14 @@
|
||||
<ul class="tabs config-overview">
|
||||
<li class="tab col s4"><a class="active" href="#overview"><i class="material-icons">info</i>
|
||||
{% trans "Overview" %}</a></li>
|
||||
<li class="tab col s4"><a href="#config"><i class="material-icons">settings</i> {%trans "Configuration" %}</a>
|
||||
<li class="tab {% if CONFIG_LOCK %}tab-locked {% endif %}col s4"><a href="#config"><i class="material-icons">{% if not CONFIG_LOCK %}settings{% else %}lock{% endif %}</i> {%trans "Configuration" %}</a>
|
||||
</li>
|
||||
<li class="tab col s4"><a href="#notify"><i class="material-icons">announcement</i> {%trans "Notifications" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="overview" class="col s12">
|
||||
{% if not CONFIG_LOCK %}
|
||||
<div class="section">
|
||||
<h5>{% trans "Getting Started" %}</h5>
|
||||
<ol>
|
||||
@@ -27,8 +28,7 @@
|
||||
{% blocktrans %}
|
||||
You can always refer to the
|
||||
<a href="https://github.com/caronc/apprise/wiki#notification-services">Apprise Wiki</a> if you're having
|
||||
troubles
|
||||
assembling your URL(s).
|
||||
troubles assembling your URL(s).
|
||||
{% endblocktrans %}
|
||||
</li>
|
||||
<li>
|
||||
@@ -44,20 +44,40 @@
|
||||
<div class="divider"></div>
|
||||
<p><strong>
|
||||
{% blocktrans %}To get started, the first thing you want to do is define your configuration. Do this by
|
||||
clicking
|
||||
on the <i>Configuration tab</i>.
|
||||
clicking on the <i>Configuration tab</i>.
|
||||
{% endblocktrans %}
|
||||
</strong></p>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="section">
|
||||
<h5>{% trans "Apprise Configuration is Locked" %}</h5>
|
||||
<p>
|
||||
{% blocktrans %}At this time, the administrator of this server has locked down all configuration. This means
|
||||
That pre-created configuration is securely hidden for the purpose of notification transmission only.
|
||||
|
||||
New configuration can not be set, and existing configuration can not be modified or viewed.
|
||||
{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="has-config">
|
||||
<div class="section">
|
||||
<h5>{% trans "Working With Your Configuration" %}</h5>
|
||||
<p>
|
||||
{% blocktrans %}The following command would cause apprise to directly notify all of your services:{% endblocktrans %}
|
||||
<br />
|
||||
<pre><code class="bash">apprise --body="Test Message" \<br/>
|
||||
apprise{% if request.is_secure %}s{% endif %}://{{request.META.HTTP_HOST}}{{BASE_URL}}/{{key}}?tags=all</code></pre>
|
||||
</p>
|
||||
{% if not CONFIG_LOCK %}
|
||||
<p>
|
||||
{% blocktrans %}The following command would cause apprise to retrieve the configuration loaded and
|
||||
send a test notification to all of your added services:{% endblocktrans %}
|
||||
<br />
|
||||
<pre><code class="bash">apprise --body="Test Message" --tag=all \<br/>
|
||||
--config={{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/get/{{key}}</code></pre>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h5>{% trans "Loaded Configuration" %}</h5>
|
||||
@@ -69,6 +89,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="config" class="col s12">
|
||||
{% if not CONFIG_LOCK %}
|
||||
<p>
|
||||
{% blocktrans %}Define your configuration below:{% endblocktrans %}
|
||||
<form id="addconfig" action="{% url "add" key %}" method="post">
|
||||
@@ -78,7 +99,13 @@
|
||||
</button>
|
||||
</form>
|
||||
</p>
|
||||
{% else %}
|
||||
<h5>{% trans "Your Configuration Is Locked" %}</h5>
|
||||
<p>{% blocktrans %}Access to your configuration has been disabled by your administrator.{% endblocktrans %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="notify" class="col s12">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
@@ -102,7 +129,8 @@
|
||||
{% endblock %}
|
||||
{% block jsfooter %}
|
||||
|
||||
async function update() {
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
async function main_init(){
|
||||
|
||||
// disable the notification tab until we know for certain
|
||||
// a notification is possible
|
||||
@@ -117,273 +145,322 @@ async function update() {
|
||||
document.querySelector('#url-list').textContent = ''
|
||||
document.querySelector('#url-list-progress').style.display = null;
|
||||
|
||||
{% if not CONFIG_LOCK %}
|
||||
// Ensure no-config sections are visible
|
||||
document.querySelector('.no-config')
|
||||
.style.display = null;
|
||||
{% endif %}
|
||||
|
||||
// perform our status check
|
||||
let response = await fetch('{% url "get" key %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
// perform a tag retrieval; start with 'all'
|
||||
let tags = ['all'];
|
||||
|
||||
let jsonResponse = await fetch('{% url "json_urls" key %}/?privacy=1', {
|
||||
method: 'GET',
|
||||
})
|
||||
if(jsonResponse.status != 200) {
|
||||
// Take an early exit
|
||||
document.querySelector('#url-list-progress').style.display = 'none';
|
||||
document.querySelector('#url-list').textContent = '{% trans "An error occurred retrieving the list of loaded Apprise URL(s)" %}'
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize our tags making it easy for an end user to
|
||||
// choose from. Tags are based off ones found in the saved
|
||||
// configuration.
|
||||
const data = await jsonResponse.json();
|
||||
let external_data = tags.concat(data.tags).reduce(function(result, item) {
|
||||
result[item] = null;
|
||||
return result;
|
||||
}, {})
|
||||
|
||||
M.Chips.init(document.querySelectorAll('.chips'), {
|
||||
placeholder: 'Optional Tag',
|
||||
secondaryPlaceholder: 'Another Tag',
|
||||
autocompleteOptions: {
|
||||
data: external_data,
|
||||
minLength: 0
|
||||
},
|
||||
onChipAdd: function(e, chip) {
|
||||
var $this = this;
|
||||
$this.chipsData.forEach(function(e, index) {
|
||||
if(!(e.tag in external_data))
|
||||
$this.deleteChip(index);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if(response.status == 204)
|
||||
{
|
||||
// no problem; we simply have no content to retrieve
|
||||
return '';
|
||||
}
|
||||
else if(response.status == 200)
|
||||
{
|
||||
// configuration found
|
||||
// Now build our our loaded list of configuration for our welcome page
|
||||
let urlList = document.createElement('ul');
|
||||
|
||||
// Remove our restrictions on sending notifications
|
||||
document.querySelector('.config-overview li a[href="#notify"]')
|
||||
.parentNode.classList.remove('disabled');
|
||||
// Create a list item for each url retrieved
|
||||
data.urls.forEach(function (entry) {
|
||||
let code = document.createElement('code');
|
||||
let li = document.createElement('li');
|
||||
code.textContent = entry.url;
|
||||
li.setAttribute('class', 'card-panel');
|
||||
li.appendChild(code);
|
||||
|
||||
// get our results
|
||||
let result = await response.json();
|
||||
// Get our tags associate with the URL
|
||||
entry.tags.forEach(function (tag) {
|
||||
let chip = document.createElement('div');
|
||||
chip.setAttribute('class', 'chip');
|
||||
chip.textContent = `🏷️ ${tag}`;
|
||||
li.appendChild(chip);
|
||||
});
|
||||
|
||||
// Set our configuration so it's visible
|
||||
document.querySelector('#id_config').value = result.config;
|
||||
// Set our format
|
||||
document.querySelector('#id_format').value = result.format;
|
||||
urlList.appendChild(li);
|
||||
});
|
||||
|
||||
// dispatch our event to update our select box
|
||||
if (typeof(Event) === 'function') {
|
||||
var event = new Event('change');
|
||||
} else { // for IE11
|
||||
var event = document.createEvent('Event');
|
||||
event.initEvent('change', true, true);
|
||||
}
|
||||
document.querySelector('#id_format').dispatchEvent(event);
|
||||
// Store our new list
|
||||
document.querySelector('#url-list-progress').style.display = 'none';
|
||||
document.querySelector('#url-list').textContent = ''
|
||||
if(urlList.childNodes.length > 0) {
|
||||
|
||||
// Ensure has-config sections are visible
|
||||
document.querySelector('.has-config')
|
||||
.style.display = null;
|
||||
|
||||
// Remove our restrictions on sending notifications
|
||||
document.querySelector('.config-overview li a[href="#notify"]')
|
||||
.parentNode.classList.remove('disabled');
|
||||
|
||||
{% if not CONFIG_LOCK %}
|
||||
// Disable any no-config entries
|
||||
document.querySelector('.no-config')
|
||||
.style.display = 'none';
|
||||
{% endif %}
|
||||
|
||||
// perform a tag retrieval; start with 'all'
|
||||
let tags = ['all'];
|
||||
// Save our list to the screen
|
||||
document.querySelector('#url-list').appendChild(urlList);
|
||||
|
||||
let jsonResponse = fetch('{% url "json_urls" key %}?privacy=1', {
|
||||
method: 'GET',
|
||||
}).then(function(jsonResponse) {
|
||||
return jsonResponse.json();
|
||||
|
||||
}).then(function (data) {
|
||||
// Initialize our tags making it easy for an end user to
|
||||
// choose from. Tags are based off ones found in the saved
|
||||
// configuration.
|
||||
let external_data = tags.concat(data.tags).reduce(function(result, item) {
|
||||
result[item] = null;
|
||||
return result;
|
||||
}, {})
|
||||
|
||||
M.Chips.init(document.querySelectorAll('.chips'), {
|
||||
placeholder: 'Optional Tag',
|
||||
secondaryPlaceholder: 'Another Tag',
|
||||
autocompleteOptions: {
|
||||
data: external_data,
|
||||
minLength: 0
|
||||
},
|
||||
onChipAdd: function(e, chip) {
|
||||
var $this = this;
|
||||
$this.chipsData.forEach(function(e, index) {
|
||||
if(!(e.tag in external_data))
|
||||
$this.deleteChip(index);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Now build our our loaded list of configuration for our welcome page
|
||||
let urlList = document.createElement('ul');
|
||||
|
||||
// Create a list item for each url retrieved
|
||||
data.urls.forEach(function (entry) {
|
||||
let code = document.createElement('code');
|
||||
let li = document.createElement('li');
|
||||
code.textContent = entry.url;
|
||||
li.setAttribute('class', 'card-panel');
|
||||
li.appendChild(code);
|
||||
|
||||
// Get our tags associate with the URL
|
||||
entry.tags.forEach(function (tag) {
|
||||
let chip = document.createElement('div');
|
||||
chip.setAttribute('class', 'chip');
|
||||
chip.textContent = `🏷️ ${tag}`;
|
||||
li.appendChild(chip);
|
||||
});
|
||||
|
||||
urlList.appendChild(li);
|
||||
});
|
||||
|
||||
// Store our new list
|
||||
document.querySelector('#url-list-progress').style.display = 'none';
|
||||
document.querySelector('#url-list').textContent = ''
|
||||
if(urlList.childNodes.length > 0) {
|
||||
document.querySelector('#url-list').appendChild(urlList);
|
||||
} else {
|
||||
document.querySelector('#url-list').textContent = '{% trans "There are no Apprise URL(s) loaded." %}'
|
||||
}
|
||||
|
||||
}).catch(function (err) {
|
||||
// There was an error
|
||||
document.querySelector('#url-list-progress').style.display = 'none';
|
||||
document.querySelector('#url-list').textContent = '{% trans "An error occurred retrieving the list of loaded Apprise URL(s)" %}'
|
||||
{% if not CONFIG_LOCK %}
|
||||
//
|
||||
// Load our configuration now into the configuration tab
|
||||
//
|
||||
let response = await fetch('{% url "get" key %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
if(response.status == 204)
|
||||
{
|
||||
// no problem; we simply have no content to retrieve
|
||||
return '';
|
||||
}
|
||||
else if(response.status == 200)
|
||||
{
|
||||
// configuration found
|
||||
|
||||
// get our results
|
||||
let result = await response.json();
|
||||
|
||||
// Set our configuration so it's visible
|
||||
document.querySelector('#id_config').value = result.config;
|
||||
// Set our format
|
||||
document.querySelector('#id_format').value = result.format;
|
||||
|
||||
// dispatch our event to update our select box
|
||||
if (typeof(Event) === 'function') {
|
||||
var event = new Event('change');
|
||||
} else { // for IE11
|
||||
var event = document.createEvent('Event');
|
||||
event.initEvent('change', true, true);
|
||||
}
|
||||
document.querySelector('#id_format').dispatchEvent(event);
|
||||
}
|
||||
{% endif %}
|
||||
} else {
|
||||
document.querySelector('#url-list').textContent = '{% trans "There are no Apprise URL(s) loaded." %}'
|
||||
}
|
||||
// if we reach here, we failed
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
update();
|
||||
function config_init() {
|
||||
// over-ride manual submit for a nicer user experience
|
||||
document.querySelector('#addconfig').onsubmit = function(event) {
|
||||
event.preventDefault();
|
||||
const form = this;
|
||||
const body = new URLSearchParams(new FormData(form));
|
||||
|
||||
// over-ride manual submit for a nicer user experience
|
||||
document.querySelector('#addconfig').onsubmit = function(event) {
|
||||
event.preventDefault();
|
||||
const form = this;
|
||||
const body = new URLSearchParams(new FormData(form));
|
||||
content = document.querySelector('#id_config')
|
||||
.value.replace(/^\s+|\s+$/gm,'');
|
||||
if(content.length) {
|
||||
// perform our status check
|
||||
let response = fetch('{% url "add" key %}', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
}).then(function(response) {
|
||||
if(response.status == 200)
|
||||
{
|
||||
// update our settings
|
||||
main_init();
|
||||
|
||||
// perform our status check
|
||||
let response = fetch('{% url "add" key %}', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
}).then(function(response) {
|
||||
if(response.status == 200)
|
||||
{
|
||||
// update our settings
|
||||
update();
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Save" %}',
|
||||
'{% trans "Successfully saved the specified URL(s)." %}',
|
||||
'success'
|
||||
);
|
||||
} else if(response.status == 500) {
|
||||
// Disk issue
|
||||
Swal.fire(
|
||||
'{% trans "Save" %}',
|
||||
'{% trans "There was an issue writing the configuration to disk. Check your file permissions and try again." %}',
|
||||
'error'
|
||||
);
|
||||
} else {
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Save" %}',
|
||||
'{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Perform Delete
|
||||
// perform our status check
|
||||
let response = fetch('{% url "del" key %}', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
}).then(function(response) {
|
||||
if(response.status == 200 || response.status == 204)
|
||||
{
|
||||
// update our settings
|
||||
main_init();
|
||||
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Save" %}',
|
||||
'{% trans "Successfully saved the specified URL(s)." %}',
|
||||
'success'
|
||||
);
|
||||
} else if(response.status == 500) {
|
||||
// Disk issue
|
||||
Swal.fire(
|
||||
'{% trans "Save" %}',
|
||||
'{% trans "There was an issue writing the configuration to disk. Check your file permissions and try again." %}',
|
||||
'error'
|
||||
);
|
||||
} else {
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Save" %}',
|
||||
'{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// over-ride manual submit for a nicer user experience
|
||||
document.querySelector('#donotify').onsubmit = function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const chipElement = document.querySelector('.chips');
|
||||
|
||||
chipElement.querySelector('.chips');
|
||||
const chipInput = document.querySelector('.chips > input');
|
||||
if(chipInput.value) {
|
||||
// This code just handles text typed in the tag section that was
|
||||
// not submitted. This forces any lingering un-committed text
|
||||
// into a tag just prior to it's submission
|
||||
const ev = new KeyboardEvent('keydown', {
|
||||
altKey:false,
|
||||
bubbles: true,
|
||||
cancelBubble: false,
|
||||
cancelable: true,
|
||||
charCode: 0,
|
||||
code: "Enter",
|
||||
composed: true,
|
||||
ctrlKey: false,
|
||||
currentTarget: null,
|
||||
defaultPrevented: true,
|
||||
detail: 0,
|
||||
eventPhase: 0,
|
||||
isComposing: false,
|
||||
isTrusted: true,
|
||||
key: "Enter",
|
||||
keyCode: 13,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
returnValue: false,
|
||||
shiftKey: false,
|
||||
type: "keydown",
|
||||
which: 13
|
||||
});
|
||||
chipInput.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
// store tags (as comma separated string) from materialize chip type into form
|
||||
document.querySelector('#id_tag').value = M.Chips.getInstance(chipElement).chipsData.reduce(
|
||||
function(s, a){
|
||||
s.push(a.tag)
|
||||
return s;
|
||||
}, []).join(",")
|
||||
|
||||
const form = this;
|
||||
const body = new URLSearchParams(new FormData(form));
|
||||
|
||||
// perform our notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "Sending notification(s)..." %}',
|
||||
);
|
||||
Swal.showLoading()
|
||||
let response = fetch('{% url "notify" key %}', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
headers: {
|
||||
'Accept': 'text/html',
|
||||
'X-Apprise-Log-Level': 'info'
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Delete" %}',
|
||||
'{% trans "Successfully removed configuration." %}',
|
||||
'success'
|
||||
);
|
||||
} else {
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Delete" %}',
|
||||
'{% trans "There was an issue removing the configuration." %}',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).then(function(response) {
|
||||
response.text().then(function (html) {
|
||||
if(response.status == 200)
|
||||
{
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "Successfully sent the notification(s)." %}' + html,
|
||||
'success'
|
||||
);
|
||||
} else if(response.status == 424) {
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "One or more of the notification(s) were not sent." %}' + html,
|
||||
'warning'
|
||||
);
|
||||
} else {
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "Failed to send the notification(s)." %}' + html,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function notify_init() {
|
||||
// over-ride manual submit for a nicer user experience
|
||||
document.querySelector('#donotify').onsubmit = function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const chipElement = document.querySelector('.chips');
|
||||
|
||||
chipElement.querySelector('.chips');
|
||||
const chipInput = document.querySelector('.chips > input');
|
||||
if(chipInput.value) {
|
||||
// This code just handles text typed in the tag section that was
|
||||
// not submitted. This forces any lingering un-committed text
|
||||
// into a tag just prior to it's submission
|
||||
const ev = new KeyboardEvent('keydown', {
|
||||
altKey:false,
|
||||
bubbles: true,
|
||||
cancelBubble: false,
|
||||
cancelable: true,
|
||||
charCode: 0,
|
||||
code: "Enter",
|
||||
composed: true,
|
||||
ctrlKey: false,
|
||||
currentTarget: null,
|
||||
defaultPrevented: true,
|
||||
detail: 0,
|
||||
eventPhase: 0,
|
||||
isComposing: false,
|
||||
isTrusted: true,
|
||||
key: "Enter",
|
||||
keyCode: 13,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
returnValue: false,
|
||||
shiftKey: false,
|
||||
type: "keydown",
|
||||
which: 13
|
||||
});
|
||||
chipInput.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
// store tags (as comma separated string) from materialize chip type into form
|
||||
document.querySelector('#id_tag').value = M.Chips.getInstance(chipElement).chipsData.reduce(
|
||||
function(s, a){
|
||||
s.push(a.tag)
|
||||
return s;
|
||||
}, []).join(",")
|
||||
|
||||
const form = this;
|
||||
const body = new URLSearchParams(new FormData(form));
|
||||
|
||||
// perform our notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "Sending notification(s)..." %}',
|
||||
);
|
||||
Swal.showLoading()
|
||||
let response = fetch('{% url "notify" key %}', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
headers: {
|
||||
'Accept': 'text/html',
|
||||
'X-Apprise-Log-Level': 'info'
|
||||
}
|
||||
}).then(function(response) {
|
||||
response.text().then(function (html) {
|
||||
if(response.status == 200)
|
||||
{
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "Successfully sent the notification(s)." %}' + html,
|
||||
'success'
|
||||
);
|
||||
} else if(response.status == 424) {
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "One or more of the notification(s) were not sent." %}' + html,
|
||||
'warning'
|
||||
);
|
||||
} else {
|
||||
// user notification
|
||||
Swal.fire(
|
||||
'{% trans "Notification" %}',
|
||||
'{% trans "Failed to send the notification(s)." %}' + html,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize our page */
|
||||
main_init();
|
||||
{% if not CONFIG_LOCK %}
|
||||
/* Initialze our configuration */
|
||||
config_init();
|
||||
{% endif %}
|
||||
notify_init();
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block onload %}
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
{{ block.super }}
|
||||
document.querySelector('label [for="id_tag"]')
|
||||
|
||||
{
|
||||
// create a new div with the class 'chips' assigned to it
|
||||
const element = document.createElement('div')
|
||||
@@ -395,4 +472,5 @@ document.querySelector('label [for="id_tag"]')
|
||||
|
||||
// Hide tag field since we use the pretty Materialize Chip setup instead
|
||||
document.querySelector('#id_tag').style.display = 'none';
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@@ -25,6 +25,7 @@
|
||||
from django.test import SimpleTestCase
|
||||
from apprise import ConfigFormat
|
||||
from unittest.mock import patch
|
||||
from django.test.utils import override_settings
|
||||
from ..forms import AUTO_DETECT_CONFIG_KEYWORD
|
||||
import json
|
||||
|
||||
@@ -38,6 +39,19 @@ class AddTests(SimpleTestCase):
|
||||
response = self.client.get('/add/**invalid-key**')
|
||||
assert response.status_code == 404
|
||||
|
||||
@override_settings(APPRISE_CONFIG_LOCK=True)
|
||||
def test_save_config_by_urls_with_lock(self):
|
||||
"""
|
||||
Test adding a configuration by URLs with lock set won't work
|
||||
"""
|
||||
# our key to use
|
||||
key = 'test_save_config_by_urls_with_lock'
|
||||
|
||||
# We simply do not have permission to do so
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key), {'urls': 'mailto://user:pass@yahoo.ca'})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_save_config_by_urls(self):
|
||||
"""
|
||||
Test adding an configuration by URLs
|
||||
@@ -99,6 +113,22 @@ class AddTests(SimpleTestCase):
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test with JSON (and no payload provided)
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data=json.dumps({}),
|
||||
content_type='application/json',
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
# Test with XML which simply isn't supported
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
data='<urls><url>mailto://user:pass@yahoo.ca</url></urls>',
|
||||
content_type='application/xml',
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
# Invalid JSON
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
|
@@ -23,6 +23,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -35,6 +36,18 @@ class DelTests(SimpleTestCase):
|
||||
response = self.client.get('/del/**invalid-key**')
|
||||
assert response.status_code == 404
|
||||
|
||||
@override_settings(APPRISE_CONFIG_LOCK=True)
|
||||
def test_del_with_lock(self):
|
||||
"""
|
||||
Test deleting a configuration by URLs with lock set won't work
|
||||
"""
|
||||
# our key to use
|
||||
key = 'test_delete_with_lock'
|
||||
|
||||
# We simply do not have permission to do so
|
||||
response = self.client.post('/del/{}'.format(key))
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_del_post(self):
|
||||
"""
|
||||
Test DEL POST
|
||||
|
@@ -23,6 +23,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -110,6 +111,23 @@ class JsonUrlsTests(SimpleTestCase):
|
||||
assert 'tags' in response.json()['urls'][0]
|
||||
assert len(response.json()['urls'][0]['tags']) == 0
|
||||
|
||||
# We can see that th URLs are not the same when the privacy flag is set
|
||||
without_privacy = \
|
||||
self.client.get('/json/urls/{}?privacy=1'.format(key))
|
||||
with_privacy = self.client.get('/json/urls/{}'.format(key))
|
||||
assert with_privacy.json()['urls'][0] != \
|
||||
without_privacy.json()['urls'][0]
|
||||
|
||||
with override_settings(APPRISE_CONFIG_LOCK=True):
|
||||
# When our configuration lock is set, our result set enforces the
|
||||
# privacy flag even if it was otherwise set:
|
||||
with_privacy = \
|
||||
self.client.get('/json/urls/{}?privacy=1'.format(key))
|
||||
|
||||
# But now they're the same under this new condition
|
||||
assert with_privacy.json()['urls'][0] == \
|
||||
without_privacy.json()['urls'][0]
|
||||
|
||||
# Add a YAML file
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key), {
|
||||
|
@@ -23,6 +23,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
from unittest.mock import patch
|
||||
from ..forms import NotifyForm
|
||||
from ..utils import ConfigCache
|
||||
@@ -36,6 +37,20 @@ class StatefulNotifyTests(SimpleTestCase):
|
||||
Test stateless notifications
|
||||
"""
|
||||
|
||||
@override_settings(APPRISE_CONFIG_LOCK=True)
|
||||
def test_stateful_configuration_with_lock(self):
|
||||
"""
|
||||
Test the retrieval of configuration when the lock is set
|
||||
"""
|
||||
# our key to use
|
||||
key = 'test_stateful_with_lock'
|
||||
|
||||
# It doesn't matter if there is or isn't any configuration; when this
|
||||
# flag is set. All that overhead is skipped and we're denied access
|
||||
# right off the bat
|
||||
response = self.client.post('/get/{}'.format(key))
|
||||
assert response.status_code == 403
|
||||
|
||||
@patch('apprise.Apprise.notify')
|
||||
def test_stateful_configuration_io(self, mock_notify):
|
||||
"""
|
||||
|
@@ -80,6 +80,7 @@ class ResponseCode(object):
|
||||
okay = 200
|
||||
no_content = 204
|
||||
bad_request = 400
|
||||
no_access = 403
|
||||
not_found = 404
|
||||
method_not_allowed = 405
|
||||
failed_dependency = 424
|
||||
@@ -125,6 +126,22 @@ class AddView(View):
|
||||
"""
|
||||
Handle a POST request
|
||||
"""
|
||||
# Detect the format our response should be in
|
||||
json_response = MIME_IS_JSON.match(request.content_type) is not None
|
||||
|
||||
if settings.APPRISE_CONFIG_LOCK:
|
||||
# General Access Control
|
||||
msg = _('The site has been configured to deny this request.')
|
||||
status = ResponseCode.no_access
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# our content
|
||||
content = {}
|
||||
if MIME_IS_FORM.match(request.content_type):
|
||||
@@ -137,7 +154,7 @@ class AddView(View):
|
||||
if form.is_valid():
|
||||
content.update(form.cleaned_data)
|
||||
|
||||
elif MIME_IS_JSON.match(request.content_type):
|
||||
elif json_response:
|
||||
# Prepare our default response
|
||||
try:
|
||||
# load our JSON content
|
||||
@@ -145,14 +162,26 @@ class AddView(View):
|
||||
|
||||
except (AttributeError, ValueError):
|
||||
# could not parse JSON response...
|
||||
return HttpResponse(
|
||||
_('Invalid JSON specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
return JsonResponse({
|
||||
'error': _('Invalid JSON specified.'),
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=ResponseCode.bad_request,
|
||||
)
|
||||
|
||||
if not content:
|
||||
return HttpResponse(
|
||||
_('The message format is not supported.'),
|
||||
status=ResponseCode.bad_request)
|
||||
# No information was posted
|
||||
msg = _('The message format is not supported.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Create ourselves an apprise object to work with
|
||||
a_obj = apprise.Apprise()
|
||||
@@ -161,27 +190,45 @@ class AddView(View):
|
||||
a_obj.add(content['urls'])
|
||||
if not len(a_obj):
|
||||
# No URLs were loaded
|
||||
return HttpResponse(
|
||||
_('No valid URLs were found.'),
|
||||
status=ResponseCode.bad_request,
|
||||
msg = _('No valid URLs were found.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
if not ConfigCache.put(
|
||||
key, '\r\n'.join([s.url() for s in a_obj]),
|
||||
apprise.ConfigFormat.TEXT):
|
||||
|
||||
return HttpResponse(
|
||||
_('The configuration could not be saved.'),
|
||||
status=ResponseCode.internal_server_error,
|
||||
msg = _('The configuration could not be saved.')
|
||||
status = ResponseCode.internal_server_error
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
elif 'config' in content:
|
||||
fmt = content.get('format', '').lower()
|
||||
if fmt not in [i[0] for i in CONFIG_FORMATS]:
|
||||
# Format must be one supported by apprise
|
||||
return HttpResponse(
|
||||
_('The format specified is invalid.'),
|
||||
status=ResponseCode.bad_request,
|
||||
msg = _('The format specified is invalid.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# prepare our apprise config object
|
||||
@@ -195,9 +242,15 @@ class AddView(View):
|
||||
# Load our configuration
|
||||
if not ac_obj.add_config(content['config'], format=fmt):
|
||||
# The format could not be detected
|
||||
return HttpResponse(
|
||||
_('The configuration format could not be detected.'),
|
||||
status=ResponseCode.bad_request,
|
||||
msg = _('The configuration format could not be detected.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Add our configuration
|
||||
@@ -206,24 +259,44 @@ class AddView(View):
|
||||
if not len(a_obj):
|
||||
# No specified URL(s) were loaded due to
|
||||
# mis-configuration on the caller's part
|
||||
return HttpResponse(
|
||||
_('No valid URL(s) were specified.'),
|
||||
status=ResponseCode.bad_request,
|
||||
msg = _('No valid URL(s) were specified.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
if not ConfigCache.put(
|
||||
key, content['config'], fmt=ac_obj[0].config_format):
|
||||
# Something went very wrong; return 500
|
||||
return HttpResponse(
|
||||
_('An error occured saving configuration.'),
|
||||
status=ResponseCode.internal_server_error,
|
||||
msg = _('An error occured saving configuration.')
|
||||
status = ResponseCode.internal_server_error
|
||||
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
else:
|
||||
# No configuration specified; we're done
|
||||
return HttpResponse(
|
||||
_('No configuration specified.'),
|
||||
status=ResponseCode.bad_request,
|
||||
)
|
||||
msg = _('No configuration specified.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# If we reach here; we successfully loaded the configuration so we can
|
||||
# go ahead and write it to disk and alert our caller of the success.
|
||||
@@ -242,19 +315,47 @@ class DelView(View):
|
||||
"""
|
||||
Handle a POST request
|
||||
"""
|
||||
# Detect the format our response should be in
|
||||
json_response = MIME_IS_JSON.match(request.content_type) is not None
|
||||
|
||||
if settings.APPRISE_CONFIG_LOCK:
|
||||
# General Access Control
|
||||
msg = _('The site has been configured to deny this request.')
|
||||
status = ResponseCode.no_access
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Clear the key
|
||||
result = ConfigCache.clear(key)
|
||||
if result is None:
|
||||
return HttpResponse(
|
||||
_('There was no configuration to remove.'),
|
||||
status=ResponseCode.no_content,
|
||||
msg = _('There was no configuration to remove.')
|
||||
status = ResponseCode.no_content
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
elif result is False:
|
||||
# There was a failure at the os level
|
||||
return HttpResponse(
|
||||
_('The configuration could not be removed.'),
|
||||
status=ResponseCode.internal_server_error,
|
||||
msg = _('The configuration could not be removed.')
|
||||
status = ResponseCode.internal_server_error
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Removed content
|
||||
@@ -277,6 +378,20 @@ class GetView(View):
|
||||
# Detect the format our response should be in
|
||||
json_response = MIME_IS_JSON.match(request.content_type) is not None
|
||||
|
||||
if settings.APPRISE_CONFIG_LOCK:
|
||||
# General Access Control
|
||||
return HttpResponse(
|
||||
_('The site has been configured to deny this request.'),
|
||||
status=ResponseCode.no_access,
|
||||
) if not json_response else JsonResponse({
|
||||
'error':
|
||||
_('The site has been configured to deny this request.')
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=ResponseCode.no_access,
|
||||
)
|
||||
|
||||
config, format = ConfigCache.get(key)
|
||||
if config is None:
|
||||
# The returned value of config and format tell a rather cryptic
|
||||
@@ -299,15 +414,15 @@ class GetView(View):
|
||||
)
|
||||
|
||||
# Something went very wrong; return 500
|
||||
return HttpResponse(
|
||||
_('An error occured accessing configuration.'),
|
||||
status=ResponseCode.internal_server_error,
|
||||
) if not json_response else JsonResponse({
|
||||
'error': _('There was no configuration found.')
|
||||
msg = _('An error occured accessing configuration.')
|
||||
status = ResponseCode.internal_server_error
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=ResponseCode.internal_server_error,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Our configuration was retrieved; now our response varies on whether
|
||||
@@ -342,6 +457,9 @@ class NotifyView(View):
|
||||
"""
|
||||
Handle a POST request
|
||||
"""
|
||||
# Detect the format our response should be in
|
||||
json_response = MIME_IS_JSON.match(request.content_type) is not None
|
||||
|
||||
# our content
|
||||
content = {}
|
||||
if MIME_IS_FORM.match(request.content_type):
|
||||
@@ -350,7 +468,7 @@ class NotifyView(View):
|
||||
if form.is_valid():
|
||||
content.update(form.cleaned_data)
|
||||
|
||||
elif MIME_IS_JSON.match(request.content_type):
|
||||
elif json_response:
|
||||
# Prepare our default response
|
||||
try:
|
||||
# load our JSON content
|
||||
@@ -358,31 +476,54 @@ class NotifyView(View):
|
||||
|
||||
except (AttributeError, ValueError):
|
||||
# could not parse JSON response...
|
||||
return HttpResponse(
|
||||
return JsonResponse(
|
||||
_('Invalid JSON specified.'),
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
if not content:
|
||||
# We could not handle the Content-Type
|
||||
return HttpResponse(
|
||||
_('The message format is not supported.'),
|
||||
status=ResponseCode.bad_request)
|
||||
msg = _('The message format is not supported.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Some basic error checking
|
||||
if not content.get('body') or \
|
||||
content.get('type', apprise.NotifyType.INFO) \
|
||||
not in apprise.NOTIFY_TYPES:
|
||||
|
||||
return HttpResponse(
|
||||
_('An invalid payload was specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
msg = _('An invalid payload was specified.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Acquire our body format (if identified)
|
||||
body_format = content.get('format', apprise.NotifyFormat.TEXT)
|
||||
if body_format and body_format not in apprise.NOTIFY_FORMATS:
|
||||
return HttpResponse(
|
||||
_('An invalid body input format was specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
msg = _('An invalid body input format was specified.')
|
||||
status = ResponseCode.bad_request
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# If we get here, we have enough information to generate a notification
|
||||
# with.
|
||||
@@ -396,15 +537,27 @@ class NotifyView(View):
|
||||
# config != None: we simply have no data
|
||||
if format is not None:
|
||||
# no content to return
|
||||
return HttpResponse(
|
||||
_('There was no configuration found.'),
|
||||
status=ResponseCode.no_content,
|
||||
msg = _('There was no configuration found.')
|
||||
status = ResponseCode.no_content
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Something went very wrong; return 500
|
||||
return HttpResponse(
|
||||
_('An error occured accessing configuration.'),
|
||||
status=ResponseCode.internal_server_error,
|
||||
msg = _('An error occured accessing configuration.')
|
||||
status = ResponseCode.internal_server_error
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Prepare ourselves a default Asset
|
||||
@@ -493,11 +646,16 @@ class NotifyView(View):
|
||||
if not result:
|
||||
# If at least one notification couldn't be sent; change up
|
||||
# the response to a 424 error code
|
||||
return HttpResponse(
|
||||
response if response is not None else
|
||||
_('One or more notification could not be sent.'),
|
||||
content_type=content_type,
|
||||
status=ResponseCode.failed_dependency)
|
||||
msg = _('One or more notification could not be sent.')
|
||||
status = ResponseCode.failed_dependency
|
||||
return HttpResponse(msg, status=status) \
|
||||
if not json_response else JsonResponse({
|
||||
'error': msg,
|
||||
},
|
||||
encoder=JSONEncoder,
|
||||
safe=False,
|
||||
status=status,
|
||||
)
|
||||
|
||||
# Return our retrieved content
|
||||
return HttpResponse(
|
||||
@@ -633,8 +791,9 @@ class JsonUrlView(View):
|
||||
|
||||
# Privacy flag
|
||||
# Support 'yes', '1', 'true', 'enable', 'active', and +
|
||||
privacy = request.GET.get('privacy', 'no')[0] in (
|
||||
'a', 'y', '1', 't', 'e', '+')
|
||||
privacy = settings.APPRISE_CONFIG_LOCK or \
|
||||
request.GET.get('privacy', 'no')[0] in (
|
||||
'a', 'y', '1', 't', 'e', '+')
|
||||
|
||||
# Optionally filter on tags. Use comma to identify more then one
|
||||
tag = request.GET.get('tag', 'all')
|
||||
|
Reference in New Issue
Block a user