Compare commits
128 Commits
latest-pan
...
master
Author | SHA1 | Date |
---|---|---|
Santiago L | 5ab4779e1a | |
Santiago L | 5e6cd2f147 | |
Santiago L | 03666d8ed0 | |
Santiago L | e88e27a56e | |
Santiago L | 9a4f4ee17c | |
Santiago L | 008f49100f | |
Santiago L | b0f77ad591 | |
Santiago L | 639ecdde58 | |
Santiago L | 361b4b41a8 | |
Santiago L | 6720df314b | |
Santiago L | 9f80c75da7 | |
Santiago L | 1258a27688 | |
Santiago L | a400c25de9 | |
Santiago L | e3ec82a182 | |
Santiago L | cda47e2fb6 | |
Santiago L | d3e5ea59a9 | |
Santiago L | b37d9cc515 | |
Santiago L | 1faab905d6 | |
Santiago L | de26baf75a | |
Santiago L | 50f916fa4d | |
Santiago L | c21a52a756 | |
Santiago L | a90e500186 | |
Santiago L | 7d6a2474ab | |
Santiago L | b365580165 | |
Santiago L | bcfed9cb79 | |
Santiago L | 867d9afe65 | |
Santiago L | e1d71fa620 | |
Santiago L | 70f7551e7d | |
Santiago L | 81c67778e5 | |
Santiago L | 9a3b6dcbc3 | |
Santiago L | 5e7a823205 | |
Santiago L | e1224ddd57 | |
Santiago L | 7b59931bcf | |
Santiago L | 0e10d2b142 | |
Santiago L | 47eb0f1efe | |
Santiago L | 28c03ac6c8 | |
Santiago L | 9953124a95 | |
Santiago L | 06c226d302 | |
Santiago L | 4f695c2e6e | |
Santiago L | e6495a967b | |
Santiago L | 6d8a2ced53 | |
Santiago L | a2927f7616 | |
Santiago L | f13fea5030 | |
Santiago L | f0683660ae | |
Santiago L | b24ddf7546 | |
Santiago L | 3b4bb51925 | |
Santiago L | a6c5aa32df | |
Santiago L | 13b4ac5eee | |
Santiago L | 8dc792b851 | |
Santiago L | 5a21f766b4 | |
Santiago L | 7183174f4c | |
Santiago L | 48ef1f21e3 | |
Santiago L | aebbd424fc | |
Santiago L | 5389f425ce | |
Santiago L | ed9bfc0eb7 | |
Santiago L | 0095da61ea | |
Santiago L | 58be94bde2 | |
Santiago L | be5e06129a | |
Santiago L | 69df9780bf | |
Santiago L | 18a41d507b | |
Santiago L | f7627926cb | |
Santiago L | ffd08459c4 | |
Santiago L | 085b8f85bd | |
Santiago L | d5fce3b6e2 | |
Santiago L | 777a7f6de5 | |
Santiago L | 422305a636 | |
Santiago L | d6cebf66a2 | |
Santiago L | 0338b927cf | |
Santiago L | 97f1c7ef2b | |
Santiago L | b6cf0c34f5 | |
Santiago L | 7fa7106d72 | |
Santiago L | 6ef7f921e9 | |
Santiago L | a8b17da992 | |
Santiago L | c689a6e44c | |
Santiago L | de979011f9 | |
Santiago L | 7d975637d5 | |
Santiago L | d863598d81 | |
Santiago L | eadc06d4c5 | |
Santiago L | 2b06652a5b | |
Santiago L | dc722ec17a | |
Santiago L | e7aabf4799 | |
Cayo Puigdefabregas | fa8a895299 | |
Cayo Puigdefabregas | 091120d3c2 | |
Cayo Puigdefabregas | c952d782cd | |
Cayo Puigdefabregas | 226327cacf | |
Cayo Puigdefabregas | 6f043cd272 | |
Cayo Puigdefabregas | 0633df114e | |
Cayo Puigdefabregas | a53b71bab1 | |
Cayo Puigdefabregas | c010c10157 | |
Santiago L | acac7727c2 | |
Cayo Puigdefabregas | 48ef6d63bc | |
Santiago L | 45bf31c9da | |
Santiago L | 08a76a8de4 | |
Santiago L | 14fbd98e33 | |
Santiago L | 58395147c9 | |
Santiago L | c505f9a3c6 | |
Santiago L | f4c0a7413c | |
Santiago L | 9d2d0befc4 | |
cayop | 350d93f820 | |
Cayo Puigdefabregas | cedb8d690b | |
Cayo Puigdefabregas | 883cf631e2 | |
Cayo Puigdefabregas | 898c6882c8 | |
Cayo Puigdefabregas | a236bbdf5d | |
Cayo Puigdefabregas | 6ce4d6b877 | |
Cayo Puigdefabregas | 8da89ae22a | |
Cayo Puigdefabregas | 78db4fb8d5 | |
Cayo Puigdefabregas | 7c62092faa | |
Cayo Puigdefabregas | d0050f81b7 | |
Cayo Puigdefabregas | 6450d0d749 | |
Cayo Puigdefabregas | 2619a50410 | |
Cayo Puigdefabregas | 24e75bc07f | |
Cayo Puigdefabregas | 0cde41042f | |
Cayo Puigdefabregas | 5b4b7310e6 | |
Cayo Puigdefabregas | 38275847d9 | |
Cayo Puigdefabregas | a4c3b00205 | |
Cayo Puigdefabregas | c386b10bc8 | |
Cayo Puigdefabregas | 0b937bfb4f | |
Cayo Puigdefabregas | 30bd1ad816 | |
Cayo Puigdefabregas | 44ebd42942 | |
Cayo Puigdefabregas | e2ef8823f8 | |
Cayo Puigdefabregas | f0fadf8bba | |
Santiago L | e6e434f525 | |
Santiago L | 43d8c9471b | |
Marc Aymerich | ea9c398de4 | |
Marc Aymerich | 6fadf0c631 | |
Marc Aymerich | a1f73d883a | |
Marc Aymerich | 0c1b4c7f4a | |
Marc Aymerich | 25fbc6a088 |
|
@ -11,7 +11,7 @@ If you are planing to do some development you may want to consider doing it unde
|
||||||
|
|
||||||
2. Build a new image, create and start a container
|
2. Build a new image, create and start a container
|
||||||
```bash
|
```bash
|
||||||
curl -L http://git.io/orchestra-Dockerfile > /tmp/Dockerfile
|
curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/Dockerfile > /tmp/Dockerfile
|
||||||
docker build -t orchestra /tmp/
|
docker build -t orchestra /tmp/
|
||||||
docker create --name orchestra -i -t -u orchestra -w /home/orchestra orchestra bash
|
docker create --name orchestra -i -t -u orchestra -w /home/orchestra orchestra bash
|
||||||
docker start orchestra
|
docker start orchestra
|
||||||
|
@ -21,12 +21,13 @@ If you are planing to do some development you may want to consider doing it unde
|
||||||
|
|
||||||
3. Deploy django-orchestra development environment, inside the container
|
3. Deploy django-orchestra development environment, inside the container
|
||||||
```bash
|
```bash
|
||||||
bash <( curl -L http://git.io/orchestra-deploy ) --dev
|
bash <( curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/deploy.sh ) --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Nginx should be serving on port 80, but Django's development server can be used as well:
|
3. Nginx should be serving on port 80, but Django's development server can be used as well:
|
||||||
```bash
|
```bash
|
||||||
cd panel
|
cd panel
|
||||||
|
python3 manage.py migrate
|
||||||
python3 manage.py runserver 0.0.0.0:8888
|
python3 manage.py runserver 0.0.0.0:8888
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -34,5 +35,5 @@ If you are planing to do some development you may want to consider doing it unde
|
||||||
5. To upgrade to current master just re-run the deploy script
|
5. To upgrade to current master just re-run the deploy script
|
||||||
```bash
|
```bash
|
||||||
git pull origin master
|
git pull origin master
|
||||||
bash <( curl -L http://git.io/orchestra-deploy ) --dev
|
bash <( curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/deploy.sh ) --dev
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
# System requirements:
|
||||||
|
The most important requirement is use python3.6
|
||||||
|
we need install this packages:
|
||||||
|
```
|
||||||
|
bind9utils
|
||||||
|
ca-certificates
|
||||||
|
gettext
|
||||||
|
libcrack2-dev
|
||||||
|
libxml2-dev
|
||||||
|
libxslt1-dev
|
||||||
|
python3
|
||||||
|
python3-pip
|
||||||
|
python3-dev
|
||||||
|
ssh-client
|
||||||
|
wget
|
||||||
|
xvfb
|
||||||
|
zlib1g-dev
|
||||||
|
git
|
||||||
|
iceweasel
|
||||||
|
dnsutils
|
||||||
|
```
|
||||||
|
We need install too a *wkhtmltopdf* package
|
||||||
|
You can use one of your OS or get it from original.
|
||||||
|
This it is in https://wkhtmltopdf.org/downloads.html
|
||||||
|
|
||||||
|
# pip installations
|
||||||
|
We need install this packages:
|
||||||
|
```
|
||||||
|
Django==1.10.5
|
||||||
|
django-fluent-dashboard==0.6.1
|
||||||
|
django-admin-tools==0.8.0
|
||||||
|
django-extensions==1.7.4
|
||||||
|
django-celery==3.1.17
|
||||||
|
celery==3.1.23
|
||||||
|
kombu==3.0.35
|
||||||
|
billiard==3.3.0.23
|
||||||
|
Markdown==2.4
|
||||||
|
djangorestframework==3.4.7
|
||||||
|
ecdsa==0.11
|
||||||
|
Pygments==1.6
|
||||||
|
django-filter==0.15.2
|
||||||
|
jsonfield==0.9.22
|
||||||
|
python-dateutil==2.2
|
||||||
|
https://github.com/glic3rinu/passlib/archive/master.zip
|
||||||
|
django-iban==0.3.0
|
||||||
|
requests
|
||||||
|
phonenumbers
|
||||||
|
django-countries
|
||||||
|
django-localflavor
|
||||||
|
amqp
|
||||||
|
anyjson
|
||||||
|
pytz
|
||||||
|
cracklib
|
||||||
|
lxml==3.3.5
|
||||||
|
selenium
|
||||||
|
xvfbwrapper
|
||||||
|
freezegun
|
||||||
|
coverage
|
||||||
|
flake8
|
||||||
|
django-debug-toolbar==1.3.0
|
||||||
|
django-nose==1.4.4
|
||||||
|
sqlparse
|
||||||
|
pyinotify
|
||||||
|
PyMySQL
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use Orchestra you need to install from pip like this:
|
||||||
|
```
|
||||||
|
pip3 install http://git.io/django-orchestra-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
But if you want develop orquestra you need to do this:
|
||||||
|
```
|
||||||
|
git clone https://github.com/ribaguifi/django-orchestra
|
||||||
|
pip install -e django-orchestra
|
||||||
|
```
|
||||||
|
|
||||||
|
# Database
|
||||||
|
For default use sqlite3 if you want to use postgresql you need install this packages:
|
||||||
|
|
||||||
|
```
|
||||||
|
psycopg2 postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use it for debian or ubuntu:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt-get install python3-psycopg2 postgresql-contrib
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember create a database for your project and give permitions for the correct user like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
psql -U postgres
|
||||||
|
psql (12.4)
|
||||||
|
Digite «help» para obtener ayuda.
|
||||||
|
|
||||||
|
postgres=# CREATE database orchesta;
|
||||||
|
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';
|
||||||
|
postgres=# GRANT ALL PRIVILEGES ON DATABASE orchesta TO orchesta;
|
||||||
|
```
|
||||||
|
|
||||||
|
# Create new project
|
||||||
|
You can use orchestra-admin for create your new project
|
||||||
|
```
|
||||||
|
orchestra-admin startproject <project_name> # e.g. panel
|
||||||
|
cd <project_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we need change the settings.py for configure the correct database
|
||||||
|
|
||||||
|
In settings.py we need change the DATABASE section like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': 'orchestra'
|
||||||
|
'USER': 'orchestra',
|
||||||
|
'PASSWORD': 'orchestra',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '5432',
|
||||||
|
'CONN_MAX_AGE': 60*10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For end you need to do the migrations:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 manage.py migrate
|
||||||
|
```
|
|
@ -3,7 +3,7 @@ from collections import OrderedDict
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -56,7 +56,7 @@ def search(request):
|
||||||
if service.search:
|
if service.search:
|
||||||
models.add(service.model)
|
models.add(service.model)
|
||||||
model_name_map[service.model._meta.model_name] = service.model
|
model_name_map[service.model._meta.model_name] = service.model
|
||||||
|
|
||||||
# Account direct access
|
# Account direct access
|
||||||
if search_term.endswith('!'):
|
if search_term.endswith('!'):
|
||||||
from ..contrib.accounts.models import Account
|
from ..contrib.accounts.models import Account
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from fluent_dashboard import dashboard, appsettings
|
from fluent_dashboard import dashboard, appsettings
|
||||||
from fluent_dashboard.modules import CmsAppIconList
|
from fluent_dashboard.modules import CmsAppIconList
|
||||||
|
@ -11,7 +11,7 @@ class AppDefaultIconList(CmsAppIconList):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.icons = kwargs.pop('icons')
|
self.icons = kwargs.pop('icons')
|
||||||
super(AppDefaultIconList, self).__init__(*args, **kwargs)
|
super(AppDefaultIconList, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_icon_for_model(self, app_name, model_name, default=None):
|
def get_icon_for_model(self, app_name, model_name, default=None):
|
||||||
icon = self.icons.get('.'.join((app_name, model_name)))
|
icon = self.icons.get('.'.join((app_name, model_name)))
|
||||||
return super(AppDefaultIconList, self).get_icon_for_model(app_name, model_name, default=icon)
|
return super(AppDefaultIconList, self).get_icon_for_model(app_name, model_name, default=icon)
|
||||||
|
@ -19,7 +19,7 @@ class AppDefaultIconList(CmsAppIconList):
|
||||||
|
|
||||||
class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
|
class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
|
||||||
""" Gets application modules from services, accounts and administration registries """
|
""" Gets application modules from services, accounts and administration registries """
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(dashboard.FluentIndexDashboard, self).__init__(**kwargs)
|
super(dashboard.FluentIndexDashboard, self).__init__(**kwargs)
|
||||||
self.children.append(self.get_personal_module())
|
self.children.append(self.get_personal_module())
|
||||||
|
@ -27,7 +27,7 @@ class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
|
||||||
recent_actions = self.get_recent_actions_module()
|
recent_actions = self.get_recent_actions_module()
|
||||||
recent_actions.enabled = True
|
recent_actions.enabled = True
|
||||||
self.children.append(recent_actions)
|
self.children.append(recent_actions)
|
||||||
|
|
||||||
def process_registered_view(self, module, view_name, options):
|
def process_registered_view(self, module, view_name, options):
|
||||||
app_name, name = view_name.split('_')[:-1]
|
app_name, name = view_name.split('_')[:-1]
|
||||||
module.icons['.'.join((app_name, name))] = options.get('icon')
|
module.icons['.'.join((app_name, name))] = options.get('icon')
|
||||||
|
@ -47,7 +47,7 @@ class OrchestraIndexDashboard(dashboard.FluentIndexDashboard):
|
||||||
'title': options.get('verbose_name_plural'),
|
'title': options.get('verbose_name_plural'),
|
||||||
'url': add_url,
|
'url': add_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_application_modules(self):
|
def get_application_modules(self):
|
||||||
modules = []
|
modules = []
|
||||||
# Honor settings override, hacky. I Know
|
# Honor settings override, hacky. I Know
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.forms.models import modelformset_factory, BaseModelFormSet
|
from django.forms.models import modelformset_factory, BaseModelFormSet
|
||||||
from django.template import Template, Context
|
from django.template import Template
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.forms.widgets import SpanWidget
|
from orchestra.forms.widgets import SpanWidget
|
||||||
|
@ -28,9 +28,9 @@ class AdminFormMixin(object):
|
||||||
' {% include "admin/includes/fieldset.html" %}'
|
' {% include "admin/includes/fieldset.html" %}'
|
||||||
'{% endfor %}'
|
'{% endfor %}'
|
||||||
)
|
)
|
||||||
context = Context({
|
context = {
|
||||||
'adminform': adminform
|
'adminform': adminform
|
||||||
})
|
}
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,9 +71,9 @@ class AdminFormSet(BaseModelFormSet):
|
||||||
</div>
|
</div>
|
||||||
</div>""")
|
</div>""")
|
||||||
)
|
)
|
||||||
context = Context({
|
context = {
|
||||||
'formset': self
|
'formset': self
|
||||||
})
|
}
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
required=False, validators=[validate_password])
|
required=False, validators=[validate_password])
|
||||||
password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput,
|
password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput,
|
||||||
required=False)
|
required=False)
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user, *args, **kwargs):
|
||||||
self.related = kwargs.pop('related', [])
|
self.related = kwargs.pop('related', [])
|
||||||
self.raw = kwargs.pop('raw', False)
|
self.raw = kwargs.pop('raw', False)
|
||||||
|
@ -109,7 +109,7 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
self.fields['password2_%i' % ix] = forms.CharField(label=_("Password (again)"),
|
self.fields['password2_%i' % ix] = forms.CharField(label=_("Password (again)"),
|
||||||
widget=forms.PasswordInput, required=False)
|
widget=forms.PasswordInput, required=False)
|
||||||
setattr(self, 'clean_password2_%i' % ix, partial(self.clean_password2, ix=ix))
|
setattr(self, 'clean_password2_%i' % ix, partial(self.clean_password2, ix=ix))
|
||||||
|
|
||||||
def clean_password2(self, ix=''):
|
def clean_password2(self, ix=''):
|
||||||
if ix != '':
|
if ix != '':
|
||||||
ix = '_%i' % ix
|
ix = '_%i' % ix
|
||||||
|
@ -129,7 +129,7 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
code='password_mismatch',
|
code='password_mismatch',
|
||||||
)
|
)
|
||||||
return password2
|
return password2
|
||||||
|
|
||||||
def clean_password(self, ix=''):
|
def clean_password(self, ix=''):
|
||||||
if ix != '':
|
if ix != '':
|
||||||
ix = '_%i' % ix
|
ix = '_%i' % ix
|
||||||
|
@ -146,14 +146,14 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
code='bad_hash',
|
code='bad_hash',
|
||||||
)
|
)
|
||||||
return password
|
return password
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if not self.password_provided:
|
if not self.password_provided:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['password_missing'],
|
self.error_messages['password_missing'],
|
||||||
code='password_missing',
|
code='password_missing',
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
"""
|
"""
|
||||||
Saves the new password.
|
Saves the new password.
|
||||||
|
@ -182,7 +182,7 @@ class AdminPasswordChangeForm(forms.Form):
|
||||||
if commit:
|
if commit:
|
||||||
rel.save(update_fields=['password'])
|
rel.save(update_fields=['password'])
|
||||||
return self.user
|
return self.user
|
||||||
|
|
||||||
def _get_changed_data(self):
|
def _get_changed_data(self):
|
||||||
data = super().changed_data
|
data = super().changed_data
|
||||||
for name in self.fields.keys():
|
for name in self.fields.keys():
|
||||||
|
@ -202,7 +202,7 @@ class SendEmailForm(forms.Form):
|
||||||
widget=forms.TextInput(attrs={'size': '118'}))
|
widget=forms.TextInput(attrs={'size': '118'}))
|
||||||
message = forms.CharField(label=_("Message"),
|
message = forms.CharField(label=_("Message"),
|
||||||
widget=forms.Textarea(attrs={'cols': 118, 'rows': 15}))
|
widget=forms.Textarea(attrs={'cols': 118, 'rows': 15}))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
initial = kwargs.get('initial')
|
initial = kwargs.get('initial')
|
||||||
|
@ -210,7 +210,7 @@ class SendEmailForm(forms.Form):
|
||||||
self.fields['to'].widget = SpanWidget(original=initial['to'])
|
self.fields['to'].widget = SpanWidget(original=initial['to'])
|
||||||
else:
|
else:
|
||||||
self.fields.pop('to')
|
self.fields.pop('to')
|
||||||
|
|
||||||
def clean_comma_separated_emails(self, value):
|
def clean_comma_separated_emails(self, value):
|
||||||
clean_value = []
|
clean_value = []
|
||||||
for email in value.split(','):
|
for email in value.split(','):
|
||||||
|
@ -222,7 +222,7 @@ class SendEmailForm(forms.Form):
|
||||||
raise validators.ValidationError("Comma separated email addresses.")
|
raise validators.ValidationError("Comma separated email addresses.")
|
||||||
clean_value.append(email)
|
clean_value.append(email)
|
||||||
return clean_value
|
return clean_value
|
||||||
|
|
||||||
def clean_extra_to(self):
|
def clean_extra_to(self):
|
||||||
extra_to = self.cleaned_data['extra_to']
|
extra_to = self.cleaned_data['extra_to']
|
||||||
return self.clean_comma_separated_emails(extra_to)
|
return self.clean_comma_separated_emails(extra_to)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from admin_tools.menu import items, Menu
|
from admin_tools.menu import items, Menu
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ def api_link(context):
|
||||||
opts = context['cl'].opts
|
opts = context['cl'].opts
|
||||||
else:
|
else:
|
||||||
return reverse('api-root')
|
return reverse('api-root')
|
||||||
if 'object_id' in context:
|
if 'object_id' in context:
|
||||||
object_id = context['object_id']
|
object_id = context['object_id']
|
||||||
try:
|
try:
|
||||||
return reverse('%s-detail' % opts.model_name, args=[object_id])
|
return reverse('%s-detail' % opts.model_name, args=[object_id])
|
||||||
|
@ -42,7 +42,7 @@ def process_registry(register):
|
||||||
item = items.MenuItem(name, url)
|
item = items.MenuItem(name, url)
|
||||||
item.options = options
|
item.options = options
|
||||||
return item
|
return item
|
||||||
|
|
||||||
childrens = {}
|
childrens = {}
|
||||||
for model, options in register.get().items():
|
for model, options in register.get().items():
|
||||||
if options.get('menu', True):
|
if options.get('menu', True):
|
||||||
|
@ -68,7 +68,7 @@ def process_registry(register):
|
||||||
|
|
||||||
class OrchestraMenu(Menu):
|
class OrchestraMenu(Menu):
|
||||||
template = 'admin/orchestra/menu.html'
|
template = 'admin/orchestra/menu.html'
|
||||||
|
|
||||||
def init_with_context(self, context):
|
def init_with_context(self, context):
|
||||||
self.children = [
|
self.children = [
|
||||||
# items.MenuItem(
|
# items.MenuItem(
|
||||||
|
|
|
@ -6,11 +6,11 @@ from functools import wraps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from orchestra.models.utils import get_field_value
|
from orchestra.models.utils import get_field_value
|
||||||
|
@ -113,21 +113,21 @@ def admin_link(*args, **kwargs):
|
||||||
return '---'
|
return '---'
|
||||||
if not getattr(obj, 'pk', None):
|
if not getattr(obj, 'pk', None):
|
||||||
return '---'
|
return '---'
|
||||||
display = kwargs.get('display')
|
display_ = kwargs.get('display')
|
||||||
if display:
|
if display_:
|
||||||
display = getattr(obj, display, display)
|
display_ = getattr(obj, display_, display_)
|
||||||
else:
|
else:
|
||||||
display = obj
|
display_ = obj
|
||||||
try:
|
try:
|
||||||
url = change_url(obj)
|
url = change_url(obj)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
# Does not has admin
|
# Does not has admin
|
||||||
return str(display)
|
return str(display_)
|
||||||
extra = ''
|
extra = ''
|
||||||
if kwargs['popup']:
|
if kwargs['popup']:
|
||||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
extra = mark_safe('onclick="return showAddAnotherPopup(this);"')
|
||||||
title = "Change %s" % obj._meta.verbose_name
|
title = "Change %s" % obj._meta.verbose_name
|
||||||
return mark_safe('<a href="%s" title="%s" %s>%s</a>' % (url, title, extra, display))
|
return format_html('<a href="{}" title="{}" {}>{}</a>', url, title, extra, display_)
|
||||||
|
|
||||||
|
|
||||||
@admin_field
|
@admin_field
|
||||||
|
@ -158,7 +158,7 @@ def admin_date(*args, **kwargs):
|
||||||
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
else:
|
else:
|
||||||
date = date.strftime("%Y-%m-%d")
|
date = date.strftime("%Y-%m-%d")
|
||||||
return '<span title="{0}">{1}</span>'.format(date, escape(natural))
|
return format_html('<span title="{0}">{1}</span>', date, natural)
|
||||||
|
|
||||||
|
|
||||||
def get_object_from_url(modeladmin, request):
|
def get_object_from_url(modeladmin, request):
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .serializers import SetPasswordSerializer
|
from .serializers import SetPasswordSerializer
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordApiMixin(object):
|
class SetPasswordApiMixin(object):
|
||||||
@detail_route(methods=['post'], serializer_class=SetPasswordSerializer)
|
@action(detail=True, methods=['post'], serializer_class=SetPasswordSerializer)
|
||||||
def set_password(self, request, pk):
|
def set_password(self, request, pk):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
data = request.data
|
data = request.data
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,16 +23,16 @@ def link_wrap(view, view_names):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def insert_links(viewset, base_name):
|
def insert_links(viewset, basename):
|
||||||
collection_links = ['api-root', '%s-list' % base_name]
|
collection_links = ['api-root', '%s-list' % basename]
|
||||||
object_links = ['api-root', '%s-list' % base_name, '%s-detail' % base_name]
|
object_links = ['api-root', '%s-list' % basename, '%s-detail' % basename]
|
||||||
exception_links = ['api-root']
|
exception_links = ['api-root']
|
||||||
list_links = ['api-root']
|
list_links = ['api-root']
|
||||||
retrieve_links = ['api-root', '%s-list' % base_name]
|
retrieve_links = ['api-root', '%s-list' % basename]
|
||||||
# Determine any `@action` or `@link` decorated methods on the viewset
|
# Determine any `@action` or `@link` decorated methods on the viewset
|
||||||
for methodname in dir(viewset):
|
for methodname in dir(viewset):
|
||||||
method = getattr(viewset, methodname)
|
method = getattr(viewset, methodname)
|
||||||
view_name = '%s-%s' % (base_name, methodname.replace('_', '-'))
|
view_name = '%s-%s' % (basename, methodname.replace('_', '-'))
|
||||||
if hasattr(method, 'collection_bind_to_methods'):
|
if hasattr(method, 'collection_bind_to_methods'):
|
||||||
list_links.append(view_name)
|
list_links.append(view_name)
|
||||||
retrieve_links.append(view_name)
|
retrieve_links.append(view_name)
|
||||||
|
|
|
@ -18,33 +18,33 @@ class LogApiMixin(object):
|
||||||
message = _('Added.')
|
message = _('Added.')
|
||||||
self.log(request, message, ADDITION, instance=self.serializer.instance)
|
self.log(request, message, ADDITION, instance=self.serializer.instance)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
""" stores serializer for accessing instance on create() """
|
""" stores serializer for accessing instance on create() """
|
||||||
super(LogApiMixin, self).perform_create(serializer)
|
super(LogApiMixin, self).perform_create(serializer)
|
||||||
self.serializer = serializer
|
self.serializer = serializer
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
from django.contrib.admin.models import CHANGE
|
from django.contrib.admin.models import CHANGE
|
||||||
response = super(LogApiMixin, self).update(request, *args, **kwargs)
|
response = super(LogApiMixin, self).update(request, *args, **kwargs)
|
||||||
message = _('Changed data')
|
message = _('Changed data')
|
||||||
self.log(request, message, CHANGE)
|
self.log(request, message, CHANGE)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
from django.contrib.admin.models import CHANGE
|
from django.contrib.admin.models import CHANGE
|
||||||
response = super(LogApiMixin, self).partial_update(request, *args, **kwargs)
|
response = super(LogApiMixin, self).partial_update(request, *args, **kwargs)
|
||||||
message = _('Changed %s') % response.data
|
message = _('Changed %s') % response.data
|
||||||
self.log(request, message, CHANGE)
|
self.log(request, message, CHANGE)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
from django.contrib.admin.models import DELETION
|
from django.contrib.admin.models import DELETION
|
||||||
message = _('Deleted')
|
message = _('Deleted')
|
||||||
self.log(request, message, DELETION)
|
self.log(request, message, DELETION)
|
||||||
response = super(LogApiMixin, self).destroy(request, *args, **kwargs)
|
response = super(LogApiMixin, self).destroy(request, *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def log(self, request, message, action, instance=None):
|
def log(self, request, message, action, instance=None):
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
instance = instance or self.get_object()
|
instance = instance or self.get_object()
|
||||||
|
@ -64,21 +64,21 @@ class LinkHeaderRouter(DefaultRouter):
|
||||||
APIRoot = import_class(settings.ORCHESTRA_API_ROOT_VIEW)
|
APIRoot = import_class(settings.ORCHESTRA_API_ROOT_VIEW)
|
||||||
APIRoot.router = self
|
APIRoot.router = self
|
||||||
return APIRoot.as_view()
|
return APIRoot.as_view()
|
||||||
|
|
||||||
def register(self, prefix, viewset, base_name=None):
|
def register(self, prefix, viewset, basename=None):
|
||||||
""" inserts link headers on every viewset """
|
""" inserts link headers on every viewset """
|
||||||
if base_name is None:
|
if basename is None:
|
||||||
base_name = self.get_default_base_name(viewset)
|
basename = self.get_default_basename(viewset)
|
||||||
insert_links(viewset, base_name)
|
insert_links(viewset, basename)
|
||||||
self.registry.append((prefix, viewset, base_name))
|
self.registry.append((prefix, viewset, basename))
|
||||||
|
|
||||||
def get_viewset(self, prefix_or_model):
|
def get_viewset(self, prefix_or_model):
|
||||||
for _prefix, viewset, __ in self.registry:
|
for _prefix, viewset, __ in self.registry:
|
||||||
if _prefix == prefix_or_model or viewset.queryset.model == prefix_or_model:
|
if _prefix == prefix_or_model or viewset.queryset.model == prefix_or_model:
|
||||||
return viewset
|
return viewset
|
||||||
msg = "%s does not have a regiestered viewset" % prefix_or_model
|
msg = "%s does not have a regiestered viewset" % prefix_or_model
|
||||||
raise KeyError(msg)
|
raise KeyError(msg)
|
||||||
|
|
||||||
def insert(self, prefix_or_model, name, field, **kwargs):
|
def insert(self, prefix_or_model, name, field, **kwargs):
|
||||||
""" Dynamically add new fields to an existing serializer """
|
""" Dynamically add new fields to an existing serializer """
|
||||||
viewset = self.get_viewset(prefix_or_model)
|
viewset = self.get_viewset(prefix_or_model)
|
||||||
|
|
|
@ -11,7 +11,7 @@ class APIRoot(views.APIView):
|
||||||
'ORCHESTRA_SITE_NAME',
|
'ORCHESTRA_SITE_NAME',
|
||||||
'ORCHESTRA_SITE_VERBOSE_NAME'
|
'ORCHESTRA_SITE_VERBOSE_NAME'
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
root_url = reverse('api-root', request=request, format=format)
|
root_url = reverse('api-root', request=request, format=format)
|
||||||
token_url = reverse('api-token-auth', request=request, format=format)
|
token_url = reverse('api-token-auth', request=request, format=format)
|
||||||
|
@ -23,7 +23,7 @@ class APIRoot(views.APIView):
|
||||||
'accountancy': {},
|
'accountancy': {},
|
||||||
'services': {},
|
'services': {},
|
||||||
}
|
}
|
||||||
if not request.user.is_anonymous():
|
if not request.user.is_anonymous:
|
||||||
list_name = '{basename}-list'
|
list_name = '{basename}-list'
|
||||||
detail_name = '{basename}-detail'
|
detail_name = '{basename}-detail'
|
||||||
for prefix, viewset, basename in self.router.registry:
|
for prefix, viewset, basename in self.router.registry:
|
||||||
|
@ -60,7 +60,7 @@ class APIRoot(views.APIView):
|
||||||
for name in self.names
|
for name in self.names
|
||||||
})
|
})
|
||||||
return Response(body, headers=headers)
|
return Response(body, headers=headers)
|
||||||
|
|
||||||
def options(self, request):
|
def options(self, request):
|
||||||
metadata = super(APIRoot, self).options(request)
|
metadata = super(APIRoot, self).options(request)
|
||||||
metadata.data['settings'] = {
|
metadata.data['settings'] = {
|
||||||
|
|
|
@ -17,7 +17,7 @@ class SetPasswordSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
""" support for postonly_fields, fields whose value can only be set on post """
|
""" support for postonly_fields, fields whose value can only be set on post """
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
""" calls model.clean() """
|
""" calls model.clean() """
|
||||||
attrs = super(HyperlinkedModelSerializer, self).validate(attrs)
|
attrs = super(HyperlinkedModelSerializer, self).validate(attrs)
|
||||||
|
@ -39,7 +39,7 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
instance = ModelClass(**validated_data)
|
instance = ModelClass(**validated_data)
|
||||||
instance.clean()
|
instance.clean()
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def post_only_cleanning(self, instance, validated_data):
|
def post_only_cleanning(self, instance, validated_data):
|
||||||
""" removes postonly_fields from attrs """
|
""" removes postonly_fields from attrs """
|
||||||
model_attrs = dict(**validated_data)
|
model_attrs = dict(**validated_data)
|
||||||
|
@ -49,12 +49,12 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
if attr in post_only_fields:
|
if attr in post_only_fields:
|
||||||
model_attrs.pop(attr)
|
model_attrs.pop(attr)
|
||||||
return model_attrs
|
return model_attrs
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
""" removes postonly_fields from attrs when not posting """
|
""" removes postonly_fields from attrs when not posting """
|
||||||
model_attrs = self.post_only_cleanning(instance, validated_data)
|
model_attrs = self.post_only_cleanning(instance, validated_data)
|
||||||
return super(HyperlinkedModelSerializer, self).update(instance, model_attrs)
|
return super(HyperlinkedModelSerializer, self).update(instance, model_attrs)
|
||||||
|
|
||||||
def partial_update(self, instance, validated_data):
|
def partial_update(self, instance, validated_data):
|
||||||
""" removes postonly_fields from attrs when not posting """
|
""" removes postonly_fields from attrs when not posting """
|
||||||
model_attrs = self.post_only_cleanning(instance, validated_data)
|
model_attrs = self.post_only_cleanning(instance, validated_data)
|
||||||
|
@ -64,7 +64,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
||||||
""" returns object on to_internal_value based on URL """
|
""" returns object on to_internal_value based on URL """
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
url = data.get('url')
|
try:
|
||||||
|
url = data.get('url')
|
||||||
|
except AttributeError:
|
||||||
|
url = None
|
||||||
if not url:
|
if not url:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'url': "URL is required."
|
'url': "URL is required."
|
||||||
|
@ -80,16 +83,16 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
||||||
password = serializers.CharField(max_length=128, label=_('Password'),
|
password = serializers.CharField(max_length=128, label=_('Password'),
|
||||||
validators=[validate_password], write_only=True, required=False,
|
validators=[validate_password], write_only=True, required=False,
|
||||||
style={'widget': widgets.PasswordInput})
|
style={'widget': widgets.PasswordInput})
|
||||||
|
|
||||||
def validate_password(self, attrs, source):
|
def validate_password(self, value):
|
||||||
""" POST only password """
|
""" POST only password """
|
||||||
if self.instance:
|
if self.instance:
|
||||||
if 'password' in attrs:
|
if value:
|
||||||
raise serializers.ValidationError(_("Can not set password"))
|
raise serializers.ValidationError(_("Can not set password"))
|
||||||
elif 'password' not in attrs:
|
elif not value:
|
||||||
raise serializers.ValidationError(_("Password required"))
|
raise serializers.ValidationError(_("Password required"))
|
||||||
return attrs
|
return value
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
""" remove password in case is not a real model field """
|
""" remove password in case is not a real model field """
|
||||||
try:
|
try:
|
||||||
|
@ -102,7 +105,7 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
||||||
if password is not None:
|
if password is not None:
|
||||||
attrs['password'] = password
|
attrs['password'] = password
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
password = validated_data.pop('password')
|
password = validated_data.pop('password')
|
||||||
instance = self.Meta.model(**validated_data)
|
instance = self.Meta.model(**validated_data)
|
||||||
|
|
|
@ -21,22 +21,22 @@ function help () {
|
||||||
|
|
||||||
function print_help () {
|
function print_help () {
|
||||||
cat <<- EOF
|
cat <<- EOF
|
||||||
|
|
||||||
${bold}NAME${normal}
|
${bold}NAME${normal}
|
||||||
${bold}orchestra-admin${normal} - Orchetsra administration script
|
${bold}orchestra-admin${normal} - Orchetsra administration script
|
||||||
|
|
||||||
${bold}OPTIONS${normal}
|
${bold}OPTIONS${normal}
|
||||||
${bold}install_requirements${normal}
|
${bold}install_requirements${normal}
|
||||||
Installs Orchestra requirements using apt-get and pip
|
Installs Orchestra requirements using apt-get and pip
|
||||||
|
|
||||||
${bold}startproject${normal}
|
${bold}startproject${normal}
|
||||||
Creates a new Django-orchestra instance
|
Creates a new Django-orchestra instance
|
||||||
|
|
||||||
${bold}help${normal}
|
${bold}help${normal}
|
||||||
Displays this help text or related help page as argument
|
Displays this help text or related help page as argument
|
||||||
for example:
|
for example:
|
||||||
${bold}orchestra-admin help startproject${normal}
|
${bold}orchestra-admin help startproject${normal}
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,17 +73,17 @@ export -f get_orchestra_dir
|
||||||
|
|
||||||
function print_install_requirements_help () {
|
function print_install_requirements_help () {
|
||||||
cat <<- EOF
|
cat <<- EOF
|
||||||
|
|
||||||
${bold}NAME${normal}
|
${bold}NAME${normal}
|
||||||
${bold}orchetsra-admin install_requirements${normal} - Installs all Orchestra requirements using apt-get and pip
|
${bold}orchetsra-admin install_requirements${normal} - Installs all Orchestra requirements using apt-get and pip
|
||||||
|
|
||||||
${bold}OPTIONS${normal}
|
${bold}OPTIONS${normal}
|
||||||
${bold}-t, --testing${normal}
|
${bold}-t, --testing${normal}
|
||||||
Install Orchestra normal requirements plus those needed for running functional tests
|
Install Orchestra normal requirements plus those needed for running functional tests
|
||||||
|
|
||||||
${bold}-h, --help${normal}
|
${bold}-h, --help${normal}
|
||||||
Displays this help text
|
Displays this help text
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ function install_requirements () {
|
||||||
opts=$(getopt -o h,t -l help,testing -- "$@") || exit 1
|
opts=$(getopt -o h,t -l help,testing -- "$@") || exit 1
|
||||||
set -- $opts
|
set -- $opts
|
||||||
testing=false
|
testing=false
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-h|--help) print_deploy_help; exit 0 ;;
|
-h|--help) print_deploy_help; exit 0 ;;
|
||||||
|
@ -105,17 +105,17 @@ function install_requirements () {
|
||||||
done
|
done
|
||||||
unset OPTIND
|
unset OPTIND
|
||||||
unset opt
|
unset opt
|
||||||
|
|
||||||
check_root || true
|
check_root || true
|
||||||
ORCHESTRA_PATH=$(get_orchestra_dir) || true
|
ORCHESTRA_PATH=$(get_orchestra_dir) || true
|
||||||
|
|
||||||
# Make sure locales are in place before installing postgres
|
# Make sure locales are in place before installing postgres
|
||||||
if [[ $({ perl --help > /dev/null; } 2>&1|grep 'locale failed') ]]; then
|
if [[ $({ perl --help > /dev/null; } 2>&1|grep 'locale failed') ]]; then
|
||||||
run sed -i "s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/" /etc/locale.gen
|
run sed -i "s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/" /etc/locale.gen
|
||||||
run locale-gen
|
run locale-gen
|
||||||
update-locale LANG=en_US.UTF-8
|
update-locale LANG=en_US.UTF-8
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# lxml: libxml2-dev, libxslt1-dev, zlib1g-dev
|
# lxml: libxml2-dev, libxslt1-dev, zlib1g-dev
|
||||||
APT="bind9utils \
|
APT="bind9utils \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
@ -136,10 +136,10 @@ function install_requirements () {
|
||||||
iceweasel \
|
iceweasel \
|
||||||
dnsutils"
|
dnsutils"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
run apt-get update
|
run apt-get update
|
||||||
run apt-get install -y $APT
|
run apt-get install -y $APT
|
||||||
|
|
||||||
# Install ca certificates before executing pip install
|
# Install ca certificates before executing pip install
|
||||||
if [[ ! -e /usr/local/share/ca-certificates/cacert.org ]]; then
|
if [[ ! -e /usr/local/share/ca-certificates/cacert.org ]]; then
|
||||||
mkdir -p /usr/local/share/ca-certificates/cacert.org
|
mkdir -p /usr/local/share/ca-certificates/cacert.org
|
||||||
|
@ -148,7 +148,7 @@ function install_requirements () {
|
||||||
http://www.cacert.org/certs/class3.crt
|
http://www.cacert.org/certs/class3.crt
|
||||||
update-ca-certificates
|
update-ca-certificates
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# cracklib and lxml are excluded on the requirements.txt because they need unconvinient system dependencies
|
# cracklib and lxml are excluded on the requirements.txt because they need unconvinient system dependencies
|
||||||
PIP="$(wget http://git.io/orchestra-requirements.txt -O - | tr '\n' ' ') \
|
PIP="$(wget http://git.io/orchestra-requirements.txt -O - | tr '\n' ' ') \
|
||||||
cracklib \
|
cracklib \
|
||||||
|
@ -157,7 +157,7 @@ function install_requirements () {
|
||||||
PIP="${PIP} \
|
PIP="${PIP} \
|
||||||
selenium \
|
selenium \
|
||||||
xvfbwrapper \
|
xvfbwrapper \
|
||||||
freezegun \
|
freezegun==0.3.14 \
|
||||||
coverage \
|
coverage \
|
||||||
flake8 \
|
flake8 \
|
||||||
django-debug-toolbar==1.3.0 \
|
django-debug-toolbar==1.3.0 \
|
||||||
|
@ -166,15 +166,15 @@ function install_requirements () {
|
||||||
pyinotify \
|
pyinotify \
|
||||||
PyMySQL"
|
PyMySQL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
run pip3 install $PIP
|
run pip3 install $PIP
|
||||||
|
|
||||||
# Install a more recent version of wkhtmltopdf (0.12.2) (PDF page number support)
|
# Install a more recent version of wkhtmltopdf (0.12.2) (PDF page number support)
|
||||||
wkhtmltox_version=$(dpkg --list | grep wkhtmltox | awk {'print $3'})
|
wkhtmltox_version=$(dpkg --list | grep wkhtmltox | awk {'print $3'})
|
||||||
minor=$(echo -e "$wkhtmltox_version\n0.12.2.1" | sort -V | head -n 1)
|
minor=$(echo -e "$wkhtmltox_version\n0.12.2.1" | sort -V | head -n 1)
|
||||||
if [[ ! $wkhtmltox_version ]] || [[ $wkhtmltox_version != 0.12.2.1 && $minor == ${wkhtmltox_version} ]]; then
|
if [[ ! $wkhtmltox_version ]] || [[ $wkhtmltox_version != 0.12.2.1 && $minor == ${wkhtmltox_version} ]]; then
|
||||||
wkhtmltox=$(mktemp)
|
wkhtmltox=$(mktemp)
|
||||||
wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb -O ${wkhtmltox}
|
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb -O ${wkhtmltox}
|
||||||
dpkg -i ${wkhtmltox} || { echo "Installing missing dependencies for wkhtmltox..." && apt-get -f -y install; }
|
dpkg -i ${wkhtmltox} || { echo "Installing missing dependencies for wkhtmltox..." && apt-get -f -y install; }
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
@ -183,30 +183,30 @@ export -f install_requirements
|
||||||
|
|
||||||
print_startproject_help () {
|
print_startproject_help () {
|
||||||
cat <<- EOF
|
cat <<- EOF
|
||||||
|
|
||||||
${bold}NAME${normal}
|
${bold}NAME${normal}
|
||||||
${bold}orchestra-admin startproject${normal} - Create a new Django-Orchestra instance
|
${bold}orchestra-admin startproject${normal} - Create a new Django-Orchestra instance
|
||||||
|
|
||||||
${bold}SYNOPSIS${normal}
|
${bold}SYNOPSIS${normal}
|
||||||
Options: [ -h ]
|
Options: [ -h ]
|
||||||
|
|
||||||
${bold}OPTIONS${normal}
|
${bold}OPTIONS${normal}
|
||||||
${bold}-h, --help${normal}
|
${bold}-h, --help${normal}
|
||||||
This help message
|
This help message
|
||||||
|
|
||||||
${bold}EXAMPLES${normal}
|
${bold}EXAMPLES${normal}
|
||||||
orchestra-admin startproject controlpanel
|
orchestra-admin startproject controlpanel
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function startproject () {
|
function startproject () {
|
||||||
local PROJECT_NAME="$2"; shift
|
local PROJECT_NAME="$2"; shift
|
||||||
|
|
||||||
opts=$(getopt -o h -l help -- "$@") || exit 1
|
opts=$(getopt -o h -l help -- "$@") || exit 1
|
||||||
set -- $opts
|
set -- $opts
|
||||||
|
|
||||||
set -- $opts
|
set -- $opts
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
@ -217,10 +217,10 @@ function startproject () {
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
unset OPTIND
|
unset OPTIND
|
||||||
unset opt
|
unset opt
|
||||||
|
|
||||||
[ $(whoami) == 'root' ] && { echo -e "\nYou don't want to run this as root\n" >&2; exit 1; }
|
[ $(whoami) == 'root' ] && { echo -e "\nYou don't want to run this as root\n" >&2; exit 1; }
|
||||||
ORCHESTRA_PATH=$(get_orchestra_dir) || { echo "Error getting orchestra dir"; exit 1; }
|
ORCHESTRA_PATH=$(get_orchestra_dir) || { echo "Error getting orchestra dir"; exit 1; }
|
||||||
if [[ ! -e $PROJECT_NAME/manage.py ]]; then
|
if [[ ! -e $PROJECT_NAME/manage.py ]]; then
|
||||||
|
|
|
@ -27,7 +27,7 @@ class crontab_parser(object):
|
||||||
_range = r'(\w+?)-(\w+)'
|
_range = r'(\w+?)-(\w+)'
|
||||||
_steps = r'/(\w+)?'
|
_steps = r'/(\w+)?'
|
||||||
_star = r'\*'
|
_star = r'\*'
|
||||||
|
|
||||||
def __init__(self, max_=60, min_=0):
|
def __init__(self, max_=60, min_=0):
|
||||||
self.max_ = max_
|
self.max_ = max_
|
||||||
self.min_ = min_
|
self.min_ = min_
|
||||||
|
@ -45,14 +45,14 @@ class crontab_parser(object):
|
||||||
raise self.ParseException('empty part')
|
raise self.ParseException('empty part')
|
||||||
acc |= set(self._parse_part(part))
|
acc |= set(self._parse_part(part))
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
def _parse_part(self, part):
|
def _parse_part(self, part):
|
||||||
for regex, handler in self.pats:
|
for regex, handler in self.pats:
|
||||||
m = regex.match(part)
|
m = regex.match(part)
|
||||||
if m:
|
if m:
|
||||||
return handler(m.groups())
|
return handler(m.groups())
|
||||||
return self._expand_range((part, ))
|
return self._expand_range((part, ))
|
||||||
|
|
||||||
def _expand_range(self, toks):
|
def _expand_range(self, toks):
|
||||||
fr = self._expand_number(toks[0])
|
fr = self._expand_number(toks[0])
|
||||||
if len(toks) > 1:
|
if len(toks) > 1:
|
||||||
|
@ -62,19 +62,19 @@ class crontab_parser(object):
|
||||||
list(range(self.min_, to + 1)))
|
list(range(self.min_, to + 1)))
|
||||||
return list(range(fr, to + 1))
|
return list(range(fr, to + 1))
|
||||||
return [fr]
|
return [fr]
|
||||||
|
|
||||||
def _range_steps(self, toks):
|
def _range_steps(self, toks):
|
||||||
if len(toks) != 3 or not toks[2]:
|
if len(toks) != 3 or not toks[2]:
|
||||||
raise self.ParseException('empty filter')
|
raise self.ParseException('empty filter')
|
||||||
return self._expand_range(toks[:2])[::int(toks[2])]
|
return self._expand_range(toks[:2])[::int(toks[2])]
|
||||||
|
|
||||||
def _star_steps(self, toks):
|
def _star_steps(self, toks):
|
||||||
if not toks or not toks[0]:
|
if not toks or not toks[0]:
|
||||||
raise self.ParseException('empty filter')
|
raise self.ParseException('empty filter')
|
||||||
return self._expand_star()[::int(toks[0])]
|
return self._expand_star()[::int(toks[0])]
|
||||||
def _expand_star(self, *args):
|
def _expand_star(self, *args):
|
||||||
return list(range(self.min_, self.max_ + self.min_))
|
return list(range(self.min_, self.max_ + self.min_))
|
||||||
|
|
||||||
def _expand_number(self, s):
|
def _expand_number(self, s):
|
||||||
if isinstance(s, str) and s[0] == '-':
|
if isinstance(s, str) and s[0] == '-':
|
||||||
raise self.ParseException('negative numbers not supported')
|
raise self.ParseException('negative numbers not supported')
|
||||||
|
@ -99,7 +99,7 @@ class Setting(object):
|
||||||
def __init__(self, manage):
|
def __init__(self, manage):
|
||||||
self.manage = manage
|
self.manage = manage
|
||||||
self.settings_file = self.get_settings_file(manage)
|
self.settings_file = self.get_settings_file(manage)
|
||||||
|
|
||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
""" get db settings from settings.py file without importing """
|
""" get db settings from settings.py file without importing """
|
||||||
settings = {'__file__': self.settings_file}
|
settings = {'__file__': self.settings_file}
|
||||||
|
@ -111,7 +111,7 @@ class Setting(object):
|
||||||
content += line
|
content += line
|
||||||
exec(content, settings)
|
exec(content, settings)
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
def get_settings_file(self, manage):
|
def get_settings_file(self, manage):
|
||||||
with open(manage, 'r') as handler:
|
with open(manage, 'r') as handler:
|
||||||
regex = re.compile(r'"DJANGO_SETTINGS_MODULE"\s*,\s*"([^"]+)"')
|
regex = re.compile(r'"DJANGO_SETTINGS_MODULE"\s*,\s*"([^"]+)"')
|
||||||
|
@ -128,7 +128,7 @@ class Setting(object):
|
||||||
class DB(object):
|
class DB(object):
|
||||||
def __init__(self, settings):
|
def __init__(self, settings):
|
||||||
self.settings = settings['DATABASES']['default']
|
self.settings = settings['DATABASES']['default']
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
if self.settings['ENGINE'] == 'django.db.backends.sqlite3':
|
if self.settings['ENGINE'] == 'django.db.backends.sqlite3':
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
@ -138,7 +138,7 @@ class DB(object):
|
||||||
self.conn = psycopg2.connect("dbname='{NAME}' user='{USER}' host='{HOST}' password='{PASSWORD}'".format(**self.settings))
|
self.conn = psycopg2.connect("dbname='{NAME}' user='{USER}' host='{HOST}' password='{PASSWORD}'".format(**self.settings))
|
||||||
else:
|
else:
|
||||||
raise ValueError("%s engine not supported." % self.settings['ENGINE'])
|
raise ValueError("%s engine not supported." % self.settings['ENGINE'])
|
||||||
|
|
||||||
def query(self, query):
|
def query(self, query):
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
try:
|
try:
|
||||||
|
@ -147,7 +147,7 @@ class DB(object):
|
||||||
finally:
|
finally:
|
||||||
cur.close()
|
cur.close()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ def fire_pending_tasks(manage, db):
|
||||||
"WHERE p.crontab_id = c.id AND p.enabled = {}"
|
"WHERE p.crontab_id = c.id AND p.enabled = {}"
|
||||||
).format(enabled)
|
).format(enabled)
|
||||||
return db.query(query)
|
return db.query(query)
|
||||||
|
|
||||||
def is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
|
def is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
|
||||||
n_minute, n_hour, n_day_of_week, n_day_of_month, n_month_of_year = now
|
n_minute, n_hour, n_day_of_week, n_day_of_month, n_month_of_year = now
|
||||||
return (
|
return (
|
||||||
|
@ -171,14 +171,14 @@ def fire_pending_tasks(manage, db):
|
||||||
n_day_of_month in crontab_parser(31, 1).parse(day_of_month) and
|
n_day_of_month in crontab_parser(31, 1).parse(day_of_month) and
|
||||||
n_month_of_year in crontab_parser(12, 1).parse(month_of_year)
|
n_month_of_year in crontab_parser(12, 1).parse(month_of_year)
|
||||||
)
|
)
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
now = tuple(map(int, now.strftime("%M %H %w %d %m").split()))
|
now = tuple(map(int, now.strftime("%M %H %w %d %m").split()))
|
||||||
for minute, hour, day_of_week, day_of_month, month_of_year, task_id in get_tasks(db):
|
for minute, hour, day_of_week, day_of_month, month_of_year, task_id in get_tasks(db):
|
||||||
if is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
|
if is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
|
||||||
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
|
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
|
||||||
manage=manage, task_id=task_id)
|
manage=manage, task_id=task_id)
|
||||||
proc = run(command, async=True)
|
proc = run(command, run_async=True)
|
||||||
yield proc
|
yield proc
|
||||||
|
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ def fire_pending_messages(settings, db):
|
||||||
MAILER_DEFERE_SECONDS = settings.get('MAILER_DEFERE_SECONDS', (300, 600, 60*60, 60*60*24))
|
MAILER_DEFERE_SECONDS = settings.get('MAILER_DEFERE_SECONDS', (300, 600, 60*60, 60*60*24))
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
query_or = []
|
query_or = []
|
||||||
|
|
||||||
for num, seconds in enumerate(MAILER_DEFERE_SECONDS):
|
for num, seconds in enumerate(MAILER_DEFERE_SECONDS):
|
||||||
delta = timedelta(seconds=seconds)
|
delta = timedelta(seconds=seconds)
|
||||||
epoch = now-delta
|
epoch = now-delta
|
||||||
|
@ -198,10 +198,10 @@ def fire_pending_messages(settings, db):
|
||||||
WHERE (mailer_message.state = 'QUEUED'
|
WHERE (mailer_message.state = 'QUEUED'
|
||||||
OR (mailer_message.state = 'DEFERRED' AND (%s))) LIMIT 1""" % ' OR '.join(query_or)
|
OR (mailer_message.state = 'DEFERRED' AND (%s))) LIMIT 1""" % ' OR '.join(query_or)
|
||||||
return bool(db.query(query))
|
return bool(db.query(query))
|
||||||
|
|
||||||
if has_pending_messages(settings, db):
|
if has_pending_messages(settings, db):
|
||||||
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
||||||
proc = run(command, async=True)
|
proc = run(command, run_async=True)
|
||||||
yield proc
|
yield proc
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ SECRET_KEY = '{{ secret_key }}'
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ INSTALLED_APPS = [
|
||||||
'admin_tools.dashboard',
|
'admin_tools.dashboard',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
|
'django_filters',
|
||||||
'passlib.ext.django',
|
'passlib.ext.django',
|
||||||
'django_countries',
|
'django_countries',
|
||||||
# 'debug_toolbar',
|
# 'debug_toolbar',
|
||||||
|
@ -84,6 +86,21 @@ INSTALLED_APPS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
|
||||||
|
'orchestra.core.caches.RequestCacheMiddleware',
|
||||||
|
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
|
||||||
|
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
ROOT_URLCONF = '{{ project_name }}.urls'
|
ROOT_URLCONF = '{{ project_name }}.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
@ -127,6 +144,24 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
||||||
|
|
||||||
|
@ -168,22 +203,6 @@ LOCALE_PATHS = (
|
||||||
ORCHESTRA_SITE_NAME = '{{ project_name }}'
|
ORCHESTRA_SITE_NAME = '{{ project_name }}'
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
# 'django.middleware.locale.LocaleMiddleware'
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'orchestra.core.caches.RequestCacheMiddleware',
|
|
||||||
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
|
|
||||||
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'accounts.Account'
|
AUTH_USER_MODEL = 'accounts.Account'
|
||||||
|
|
||||||
|
|
||||||
|
@ -228,7 +247,7 @@ REST_FRAMEWORK = {
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
),
|
),
|
||||||
'DEFAULT_FILTER_BACKENDS': (
|
'DEFAULT_FILTER_BACKENDS': (
|
||||||
('rest_framework.filters.DjangoFilterBackend',)
|
('django_filters.rest_framework.DjangoFilterBackend',)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +261,6 @@ PASSLIB_CONFIG = (
|
||||||
"default = sha512_crypt\n"
|
"default = sha512_crypt\n"
|
||||||
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
||||||
" django_des_crypt, des_crypt, hex_md5\n"
|
" django_des_crypt, des_crypt, hex_md5\n"
|
||||||
"all__vary_rounds = 0.05\n"
|
|
||||||
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
||||||
"sha512_crypt__min_rounds = 80000\n"
|
"sha512_crypt__min_rounds = 80000\n"
|
||||||
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.contrib.admin.utils import NestedObjects, quote
|
from django.contrib.admin.utils import NestedObjects, quote
|
||||||
from django.contrib.auth import get_permission_codename
|
from django.contrib.auth import get_permission_codename
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.db import router
|
from django.db import router
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
@ -53,14 +53,14 @@ def service_report(modeladmin, request, queryset):
|
||||||
fields.append((model, name))
|
fields.append((model, name))
|
||||||
fields = sorted(fields, key=lambda f: f[0]._meta.verbose_name_plural.lower())
|
fields = sorted(fields, key=lambda f: f[0]._meta.verbose_name_plural.lower())
|
||||||
fields = [field for model, field in fields]
|
fields = [field for model, field in fields]
|
||||||
|
|
||||||
for account in queryset.prefetch_related(*fields):
|
for account in queryset.prefetch_related(*fields):
|
||||||
items = []
|
items = []
|
||||||
for field in fields:
|
for field in fields:
|
||||||
related_manager = getattr(account, field)
|
related_manager = getattr(account, field)
|
||||||
items.append((related_manager.model._meta, related_manager.all()))
|
items.append((related_manager.model._meta, related_manager.all()))
|
||||||
accounts.append((account, items))
|
accounts.append((account, items))
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'accounts': accounts,
|
'accounts': accounts,
|
||||||
'date': timezone.now().today()
|
'date': timezone.now().today()
|
||||||
|
@ -71,21 +71,21 @@ def service_report(modeladmin, request, queryset):
|
||||||
def delete_related_services(modeladmin, request, queryset):
|
def delete_related_services(modeladmin, request, queryset):
|
||||||
opts = modeladmin.model._meta
|
opts = modeladmin.model._meta
|
||||||
app_label = opts.app_label
|
app_label = opts.app_label
|
||||||
|
|
||||||
using = router.db_for_write(modeladmin.model)
|
using = router.db_for_write(modeladmin.model)
|
||||||
collector = NestedObjects(using=using)
|
collector = NestedObjects(using=using)
|
||||||
collector.collect(queryset)
|
collector.collect(queryset)
|
||||||
registered_services = services.get()
|
registered_services = services.get()
|
||||||
related_services = []
|
related_services = []
|
||||||
to_delete = []
|
to_delete = []
|
||||||
|
|
||||||
admin_site = modeladmin.admin_site
|
admin_site = modeladmin.admin_site
|
||||||
|
|
||||||
def format(obj, account=False):
|
def format(obj, account=False):
|
||||||
has_admin = obj.__class__ in admin_site._registry
|
has_admin = obj.__class__ in admin_site._registry
|
||||||
opts = obj._meta
|
opts = obj._meta
|
||||||
no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), force_text(obj))
|
no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), force_text(obj))
|
||||||
|
|
||||||
if has_admin:
|
if has_admin:
|
||||||
try:
|
try:
|
||||||
admin_url = reverse(
|
admin_url = reverse(
|
||||||
|
@ -95,7 +95,7 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
# Change url doesn't exist -- don't display link to edit
|
# Change url doesn't exist -- don't display link to edit
|
||||||
return no_edit_link
|
return no_edit_link
|
||||||
|
|
||||||
# Display a link to the admin page.
|
# Display a link to the admin page.
|
||||||
context = (capfirst(opts.verbose_name), admin_url, obj)
|
context = (capfirst(opts.verbose_name), admin_url, obj)
|
||||||
if account:
|
if account:
|
||||||
|
@ -106,7 +106,7 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
# Don't display link to edit, because it either has no
|
# Don't display link to edit, because it either has no
|
||||||
# admin or is edited inline.
|
# admin or is edited inline.
|
||||||
return no_edit_link
|
return no_edit_link
|
||||||
|
|
||||||
def format_nested(objs, result):
|
def format_nested(objs, result):
|
||||||
if isinstance(objs, list):
|
if isinstance(objs, list):
|
||||||
current = []
|
current = []
|
||||||
|
@ -115,7 +115,7 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
result.append(current)
|
result.append(current)
|
||||||
else:
|
else:
|
||||||
result.append(format(objs))
|
result.append(format(objs))
|
||||||
|
|
||||||
for nested in collector.nested():
|
for nested in collector.nested():
|
||||||
if isinstance(nested, list):
|
if isinstance(nested, list):
|
||||||
# Is lists of objects
|
# Is lists of objects
|
||||||
|
@ -141,7 +141,7 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
# Prevent the deletion of the main system user, which will delete the account
|
# Prevent the deletion of the main system user, which will delete the account
|
||||||
main_systemuser = nested.main_systemuser
|
main_systemuser = nested.main_systemuser
|
||||||
related_services.append(format(nested, account=True))
|
related_services.append(format(nested, account=True))
|
||||||
|
|
||||||
# The user has already confirmed the deletion.
|
# The user has already confirmed the deletion.
|
||||||
# Do the deletion and return a None to display the change list view again.
|
# Do the deletion and return a None to display the change list view again.
|
||||||
if request.POST.get('post'):
|
if request.POST.get('post'):
|
||||||
|
@ -165,17 +165,17 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
modeladmin.message_user(request, msg, messages.SUCCESS)
|
modeladmin.message_user(request, msg, messages.SUCCESS)
|
||||||
# Return None to display the change list page again.
|
# Return None to display the change list page again.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if len(queryset) == 1:
|
if len(queryset) == 1:
|
||||||
objects_name = force_text(opts.verbose_name)
|
objects_name = force_text(opts.verbose_name)
|
||||||
else:
|
else:
|
||||||
objects_name = force_text(opts.verbose_name_plural)
|
objects_name = force_text(opts.verbose_name_plural)
|
||||||
|
|
||||||
model_count = {}
|
model_count = {}
|
||||||
for model, objs in collector.model_objs.items():
|
for model, objs in collector.model_objs.items():
|
||||||
count = 0
|
count = 0
|
||||||
# discount main systemuser
|
# discount main systemuser
|
||||||
if model is modeladmin.model.main_systemuser.field.rel.to:
|
if model is modeladmin.model.main_systemuser.field.related_model:
|
||||||
count = len(objs) - 1
|
count = len(objs) - 1
|
||||||
# Discount account
|
# Discount account
|
||||||
elif model is not modeladmin.model and model in registered_services:
|
elif model is not modeladmin.model and model in registered_services:
|
||||||
|
@ -220,10 +220,10 @@ def disable_selected(modeladmin, request, queryset, disable=True):
|
||||||
n)
|
n)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
admin_site = modeladmin.admin_site
|
admin_site = modeladmin.admin_site
|
||||||
|
|
||||||
def format(obj):
|
def format(obj):
|
||||||
has_admin = obj.__class__ in admin_site._registry
|
has_admin = obj.__class__ in admin_site._registry
|
||||||
opts = obj._meta
|
opts = obj._meta
|
||||||
|
@ -238,7 +238,7 @@ def disable_selected(modeladmin, request, queryset, disable=True):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
# Change url doesn't exist -- don't display link to edit
|
# Change url doesn't exist -- don't display link to edit
|
||||||
return no_edit_link
|
return no_edit_link
|
||||||
|
|
||||||
p = '%s.%s' % (opts.app_label, get_permission_codename('delete', opts))
|
p = '%s.%s' % (opts.app_label, get_permission_codename('delete', opts))
|
||||||
if not user.has_perm(p):
|
if not user.has_perm(p):
|
||||||
perms_needed.add(opts.verbose_name)
|
perms_needed.add(opts.verbose_name)
|
||||||
|
@ -249,19 +249,19 @@ def disable_selected(modeladmin, request, queryset, disable=True):
|
||||||
# Don't display link to edit, because it either has no
|
# Don't display link to edit, because it either has no
|
||||||
# admin or is edited inline.
|
# admin or is edited inline.
|
||||||
return no_edit_link
|
return no_edit_link
|
||||||
|
|
||||||
display = []
|
display = []
|
||||||
for account in queryset:
|
for account in queryset:
|
||||||
current = []
|
current = []
|
||||||
for related in account.get_services_to_disable():
|
for related in account.get_services_to_disable():
|
||||||
current.append(format(related))
|
current.append(format(related))
|
||||||
display.append([format(account), current])
|
display.append([format(account), current])
|
||||||
|
|
||||||
if len(queryset) == 1:
|
if len(queryset) == 1:
|
||||||
objects_name = force_text(opts.verbose_name)
|
objects_name = force_text(opts.verbose_name)
|
||||||
else:
|
else:
|
||||||
objects_name = force_text(opts.verbose_name_plural)
|
objects_name = force_text(opts.verbose_name_plural)
|
||||||
|
|
||||||
context = dict(
|
context = dict(
|
||||||
admin_site.each_context(request),
|
admin_site.each_context(request),
|
||||||
action_name='disable_selected' if disable else 'enable_selected',
|
action_name='disable_selected' if disable else 'enable_selected',
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.conf.urls import url
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.contrib.auth import admin as auth
|
from django.contrib.auth import admin as auth
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -71,15 +71,15 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
||||||
)
|
)
|
||||||
change_view_actions = (disable_selected, service_report, enable_selected)
|
change_view_actions = (disable_selected, service_report, enable_selected)
|
||||||
ordering = ()
|
ordering = ()
|
||||||
|
|
||||||
main_systemuser_link = admin_link('main_systemuser')
|
main_systemuser_link = admin_link('main_systemuser')
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'comments':
|
if db_field.name == 'comments':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
||||||
return super(AccountAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(AccountAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
if not add:
|
if not add:
|
||||||
if request.method == 'GET' and not obj.is_active:
|
if request.method == 'GET' and not obj.is_active:
|
||||||
|
@ -96,7 +96,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
||||||
})
|
})
|
||||||
return super(AccountAdmin, self).render_change_form(
|
return super(AccountAdmin, self).render_change_form(
|
||||||
request, context, add, change, form_url, obj)
|
request, context, add, change, form_url, obj)
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
|
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
|
||||||
if not obj:
|
if not obj:
|
||||||
|
@ -106,7 +106,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
||||||
fieldsets = list(fieldsets)
|
fieldsets = list(fieldsets)
|
||||||
fieldsets.insert(1, (_("Related services"), {'fields': fields}))
|
fieldsets.insert(1, (_("Related services"), {'fields': fields}))
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
if not change:
|
if not change:
|
||||||
form.save_model(obj)
|
form.save_model(obj)
|
||||||
|
@ -133,7 +133,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
||||||
if msg:
|
if msg:
|
||||||
messages.warning(request, mark_safe(msg % context))
|
messages.warning(request, mark_safe(msg % context))
|
||||||
super(AccountAdmin, self).save_model(request, obj, form, change)
|
super(AccountAdmin, self).save_model(request, obj, form, change)
|
||||||
|
|
||||||
def get_change_view_actions(self, obj=None):
|
def get_change_view_actions(self, obj=None):
|
||||||
views = super().get_change_view_actions(obj=obj)
|
views = super().get_change_view_actions(obj=obj)
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
|
@ -141,7 +141,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
||||||
return [view for view in views if view.url_name != 'enable']
|
return [view for view in views if view.url_name != 'enable']
|
||||||
return [view for view in views if view.url_name != 'disable']
|
return [view for view in views if view.url_name != 'disable']
|
||||||
return views
|
return views
|
||||||
|
|
||||||
def get_actions(self, request):
|
def get_actions(self, request):
|
||||||
actions = super().get_actions(request)
|
actions = super().get_actions(request)
|
||||||
if 'delete_selected' in actions:
|
if 'delete_selected' in actions:
|
||||||
|
@ -157,7 +157,8 @@ class AccountListAdmin(AccountAdmin):
|
||||||
list_display = ('select_account', 'username', 'type', 'username')
|
list_display = ('select_account', 'username', 'type', 'username')
|
||||||
actions = None
|
actions = None
|
||||||
change_list_template = 'admin/accounts/account/select_account_list.html'
|
change_list_template = 'admin/accounts/account/select_account_list.html'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def select_account(self, instance):
|
def select_account(self, instance):
|
||||||
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
||||||
context = {
|
context = {
|
||||||
|
@ -167,9 +168,8 @@ class AccountListAdmin(AccountAdmin):
|
||||||
}
|
}
|
||||||
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
||||||
select_account.short_description = _("account")
|
select_account.short_description = _("account")
|
||||||
select_account.allow_tags = True
|
|
||||||
select_account.admin_order_field = 'username'
|
select_account.admin_order_field = 'username'
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
app_label = request.META['PATH_INFO'].split('/')[-5]
|
app_label = request.META['PATH_INFO'].split('/')[-5]
|
||||||
model = request.META['PATH_INFO'].split('/')[-4]
|
model = request.META['PATH_INFO'].split('/')[-4]
|
||||||
|
@ -206,7 +206,8 @@ class AccountAdminMixin(object):
|
||||||
change_form_template = 'admin/accounts/account/change_form.html'
|
change_form_template = 'admin/accounts/account/change_form.html'
|
||||||
account = None
|
account = None
|
||||||
list_select_related = ('account',)
|
list_select_related = ('account',)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_active(self, instance):
|
def display_active(self, instance):
|
||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
||||||
|
@ -215,16 +216,14 @@ class AccountAdminMixin(object):
|
||||||
return '<img style="width:13px" src="%s" alt="False" title="%s">' % (static('admin/img/inline-delete.svg'), msg)
|
return '<img style="width:13px" src="%s" alt="False" title="%s">' % (static('admin/img/inline-delete.svg'), msg)
|
||||||
return '<img src="%s" alt="False">' % static('admin/img/icon-yes.svg')
|
return '<img src="%s" alt="False">' % static('admin/img/icon-yes.svg')
|
||||||
display_active.short_description = _("active")
|
display_active.short_description = _("active")
|
||||||
display_active.allow_tags = True
|
|
||||||
display_active.admin_order_field = 'is_active'
|
display_active.admin_order_field = 'is_active'
|
||||||
|
|
||||||
def account_link(self, instance):
|
def account_link(self, instance):
|
||||||
account = instance.account if instance.pk else self.account
|
account = instance.account if instance.pk else self.account
|
||||||
return admin_link()(account)
|
return admin_link()(account)
|
||||||
account_link.short_description = _("account")
|
account_link.short_description = _("account")
|
||||||
account_link.allow_tags = True
|
|
||||||
account_link.admin_order_field = 'account__username'
|
account_link.admin_order_field = 'account__username'
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
""" Warns user when object's account is disabled """
|
""" Warns user when object's account is disabled """
|
||||||
form = super(AccountAdminMixin, self).get_form(request, obj, **kwargs)
|
form = super(AccountAdminMixin, self).get_form(request, obj, **kwargs)
|
||||||
|
@ -247,7 +246,7 @@ class AccountAdminMixin(object):
|
||||||
# Not available in POST
|
# Not available in POST
|
||||||
form.initial_account = self.get_changeform_initial_data(request).get('account')
|
form.initial_account = self.get_changeform_initial_data(request).get('account')
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def get_fields(self, request, obj=None):
|
def get_fields(self, request, obj=None):
|
||||||
""" remove account or account_link depending on the case """
|
""" remove account or account_link depending on the case """
|
||||||
fields = super(AccountAdminMixin, self).get_fields(request, obj)
|
fields = super(AccountAdminMixin, self).get_fields(request, obj)
|
||||||
|
@ -263,13 +262,13 @@ class AccountAdminMixin(object):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
""" provide account for filter_by_account_fields """
|
""" provide account for filter_by_account_fields """
|
||||||
if obj:
|
if obj:
|
||||||
self.account = obj.account
|
self.account = obj.account
|
||||||
return super(AccountAdminMixin, self).get_readonly_fields(request, obj)
|
return super(AccountAdminMixin, self).get_readonly_fields(request, obj)
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Filter by account """
|
""" Filter by account """
|
||||||
formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)
|
formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
@ -277,14 +276,14 @@ class AccountAdminMixin(object):
|
||||||
if self.account:
|
if self.account:
|
||||||
# Hack widget render in order to append ?account=id to the add url
|
# Hack widget render in order to append ?account=id to the add url
|
||||||
old_render = formfield.widget.render
|
old_render = formfield.widget.render
|
||||||
|
|
||||||
def render(*args, **kwargs):
|
def render(*args, **kwargs):
|
||||||
output = old_render(*args, **kwargs)
|
output = old_render(*args, **kwargs)
|
||||||
output = output.replace('/add/"', '/add/?account=%s"' % self.account.pk)
|
output = output.replace('/add/"', '/add/?account=%s"' % self.account.pk)
|
||||||
with_qargs = r'/add/?\1&account=%s"' % self.account.pk
|
with_qargs = r'/add/?\1&account=%s"' % self.account.pk
|
||||||
output = re.sub(r'/add/\?([^".]*)"', with_qargs, output)
|
output = re.sub(r'/add/\?([^".]*)"', with_qargs, output)
|
||||||
return mark_safe(output)
|
return mark_safe(output)
|
||||||
|
|
||||||
formfield.widget.render = render
|
formfield.widget.render = render
|
||||||
# Filter related object by account
|
# Filter related object by account
|
||||||
formfield.queryset = formfield.queryset.filter(account=self.account)
|
formfield.queryset = formfield.queryset.filter(account=self.account)
|
||||||
|
@ -302,21 +301,21 @@ class AccountAdminMixin(object):
|
||||||
formfield.initial = 1
|
formfield.initial = 1
|
||||||
formfield.queryset = formfield.queryset.order_by('username')
|
formfield.queryset = formfield.queryset.order_by('username')
|
||||||
return formfield
|
return formfield
|
||||||
|
|
||||||
def get_formset(self, request, obj=None, **kwargs):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
""" provides form.account for convinience """
|
""" provides form.account for convinience """
|
||||||
formset = super(AccountAdminMixin, self).get_formset(request, obj, **kwargs)
|
formset = super(AccountAdminMixin, self).get_formset(request, obj, **kwargs)
|
||||||
formset.form.account = self.account
|
formset.form.account = self.account
|
||||||
formset.account = self.account
|
formset.account = self.account
|
||||||
return formset
|
return formset
|
||||||
|
|
||||||
def get_account_from_preserve_filters(self, request):
|
def get_account_from_preserve_filters(self, request):
|
||||||
preserved_filters = self.get_preserved_filters(request)
|
preserved_filters = self.get_preserved_filters(request)
|
||||||
preserved_filters = dict(parse_qsl(preserved_filters))
|
preserved_filters = dict(parse_qsl(preserved_filters))
|
||||||
cl_filters = preserved_filters.get('_changelist_filters')
|
cl_filters = preserved_filters.get('_changelist_filters')
|
||||||
if cl_filters:
|
if cl_filters:
|
||||||
return dict(parse_qsl(cl_filters)).get('account')
|
return dict(parse_qsl(cl_filters)).get('account')
|
||||||
|
|
||||||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||||
account_id = self.get_account_from_preserve_filters(request)
|
account_id = self.get_account_from_preserve_filters(request)
|
||||||
if not object_id:
|
if not object_id:
|
||||||
|
@ -331,7 +330,7 @@ class AccountAdminMixin(object):
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(AccountAdminMixin, self).changeform_view(
|
return super(AccountAdminMixin, self).changeform_view(
|
||||||
request, object_id, form_url=form_url, extra_context=context)
|
request, object_id, form_url=form_url, extra_context=context)
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
account_id = request.GET.get('account')
|
account_id = request.GET.get('account')
|
||||||
context = {}
|
context = {}
|
||||||
|
@ -367,7 +366,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
account = Account.objects.get(pk=request.GET['account'])
|
account = Account.objects.get(pk=request.GET['account'])
|
||||||
[setattr(inline, 'account', account) for inline in inlines]
|
[setattr(inline, 'account', account) for inline in inlines]
|
||||||
return inlines
|
return inlines
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
""" Hooks select account url """
|
""" Hooks select account url """
|
||||||
urls = super(AccountAdminMixin, self).get_urls()
|
urls = super(AccountAdminMixin, self).get_urls()
|
||||||
|
@ -381,7 +380,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
name='%s_%s_select_account' % info),
|
name='%s_%s_select_account' % info),
|
||||||
]
|
]
|
||||||
return select_urls + urls
|
return select_urls + urls
|
||||||
|
|
||||||
def add_view(self, request, form_url='', extra_context=None):
|
def add_view(self, request, form_url='', extra_context=None):
|
||||||
""" Redirects to select account view if required """
|
""" Redirects to select account view if required """
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
|
@ -406,7 +405,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
return super(AccountAdminMixin, self).add_view(
|
return super(AccountAdminMixin, self).add_view(
|
||||||
request, form_url=form_url, extra_context=context)
|
request, form_url=form_url, extra_context=context)
|
||||||
return HttpResponseRedirect('./select-account/?%s' % request.META['QUERY_STRING'])
|
return HttpResponseRedirect('./select-account/?%s' % request.META['QUERY_STRING'])
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
"""
|
"""
|
||||||
Given a model instance save it to the database.
|
Given a model instance save it to the database.
|
||||||
|
|
|
@ -34,7 +34,7 @@ def create_account_creation_form():
|
||||||
fields[field_name] = forms.BooleanField(
|
fields[field_name] = forms.BooleanField(
|
||||||
initial=True, required=False, label=label, help_text=help_text)
|
initial=True, required=False, label=label, help_text=help_text)
|
||||||
create_related.append((model, key, kwargs, help_text))
|
create_related.append((model, key, kwargs, help_text))
|
||||||
|
|
||||||
def clean(self, create_related=create_related):
|
def clean(self, create_related=create_related):
|
||||||
""" unique usernames between accounts and system users """
|
""" unique usernames between accounts and system users """
|
||||||
cleaned_data = UserCreationForm.clean(self)
|
cleaned_data = UserCreationForm.clean(self)
|
||||||
|
@ -47,7 +47,7 @@ def create_account_creation_form():
|
||||||
# Previous validation error
|
# Previous validation error
|
||||||
return
|
return
|
||||||
errors = {}
|
errors = {}
|
||||||
systemuser_model = Account.main_systemuser.field.rel.to
|
systemuser_model = Account.main_systemuser.field.related_model
|
||||||
if systemuser_model.objects.filter(username=account.username).exists():
|
if systemuser_model.objects.filter(username=account.username).exists():
|
||||||
errors['username'] = _("A system user with this name already exists.")
|
errors['username'] = _("A system user with this name already exists.")
|
||||||
for model, key, related_kwargs, __ in create_related:
|
for model, key, related_kwargs, __ in create_related:
|
||||||
|
@ -62,11 +62,11 @@ def create_account_creation_form():
|
||||||
params={'type': verbose_name})
|
params={'type': verbose_name})
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
def save_model(self, account):
|
def save_model(self, account):
|
||||||
enable_systemuser=self.cleaned_data['enable_systemuser']
|
enable_systemuser=self.cleaned_data['enable_systemuser']
|
||||||
account.save(active_systemuser=enable_systemuser)
|
account.save(active_systemuser=enable_systemuser)
|
||||||
|
|
||||||
def save_related(self, account):
|
def save_related(self, account):
|
||||||
for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
|
for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
|
||||||
model = apps.get_model(model)
|
model = apps.get_model(model)
|
||||||
|
@ -76,14 +76,14 @@ def create_account_creation_form():
|
||||||
key: eval(value, {'account': account}) for key, value in related_kwargs.items()
|
key: eval(value, {'account': account}) for key, value in related_kwargs.items()
|
||||||
}
|
}
|
||||||
model.objects.create(account=account, **kwargs)
|
model.objects.create(account=account, **kwargs)
|
||||||
|
|
||||||
fields.update({
|
fields.update({
|
||||||
'create_related_fields': list(fields.keys()),
|
'create_related_fields': list(fields.keys()),
|
||||||
'clean': clean,
|
'clean': clean,
|
||||||
'save_model': save_model,
|
'save_model': save_model,
|
||||||
'save_related': save_related,
|
'save_related': save_related,
|
||||||
})
|
})
|
||||||
|
|
||||||
return type('AccountCreationForm', (UserCreationForm,), fields)
|
return type('AccountCreationForm', (UserCreationForm,), fields)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model, base_user
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -19,9 +19,14 @@ def create_initial_superuser(**kwargs):
|
||||||
)
|
)
|
||||||
from ..models import Account
|
from ..models import Account
|
||||||
try:
|
try:
|
||||||
Account.systemusers.field.related.model.objects.filter(account_id=1).exists()
|
Account.systemusers.field.model.objects.filter(account_id=1).exists()
|
||||||
except FieldError:
|
except FieldError:
|
||||||
# avoid creating a systemuser when systemuser table is not ready
|
# avoid creating a systemuser when systemuser table is not ready
|
||||||
Account.save = models.Model.save
|
Account.save = models.Model.save
|
||||||
|
old_init = base_user.AbstractBaseUser.__init__
|
||||||
|
def remove_is_staff(*args, **kwargs):
|
||||||
|
kwargs.pop('is_staff', None)
|
||||||
|
old_init(*args, **kwargs)
|
||||||
|
base_user.AbstractBaseUser.__init__ = remove_is_staff
|
||||||
manager = sys.argv[0]
|
manager = sys.argv[0]
|
||||||
execute_from_command_line(argv=[manager, 'createsuperuser'])
|
execute_from_command_line(argv=[manager, 'createsuperuser'])
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ class Migration(migrations.Migration):
|
||||||
('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')),
|
('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')),
|
||||||
('is_active', models.BooleanField(help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')),
|
('is_active', models.BooleanField(help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')),
|
||||||
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
|
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
|
||||||
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, related_name='accounts_main')),
|
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:08
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import orchestra.contrib.accounts.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('accounts', '0001_initial'), ('accounts', '0002_auto_20170528_2005'), ('accounts', '0003_auto_20210330_1049'), ('accounts', '0004_auto_20210422_1108')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('systemusers', '0001_initial'),
|
||||||
|
('auth', '0006_require_contenttypes_0002'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Account',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('username', models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username')),
|
||||||
|
('short_name', models.CharField(blank=True, max_length=64, verbose_name='short name')),
|
||||||
|
('full_name', models.CharField(max_length=256, verbose_name='full name')),
|
||||||
|
('email', models.EmailField(help_text='Used for password recovery', max_length=254, verbose_name='email address')),
|
||||||
|
('type', models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type')),
|
||||||
|
('language', models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language')),
|
||||||
|
('comments', models.TextField(blank=True, max_length=256, verbose_name='comments')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('main_systemuser', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main', to='systemusers.SystemUser')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='account',
|
||||||
|
managers=[
|
||||||
|
('objects', orchestra.contrib.accounts.models.AccountManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='main_systemuser',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts_main', to='systemusers.SystemUser'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_auto_20170528_2005'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -29,7 +29,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
|
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
|
||||||
])
|
])
|
||||||
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
|
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
|
||||||
related_name='accounts_main', editable=False)
|
related_name='accounts_main', editable=False, on_delete=models.SET_NULL)
|
||||||
short_name = models.CharField(_("short name"), max_length=64, blank=True)
|
short_name = models.CharField(_("short name"), max_length=64, blank=True)
|
||||||
full_name = models.CharField(_("full name"), max_length=256)
|
full_name = models.CharField(_("full name"), max_length=256)
|
||||||
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
||||||
|
@ -46,23 +46,28 @@ class Account(auth.AbstractBaseUser):
|
||||||
help_text=_("Designates whether this account should be treated as active. "
|
help_text=_("Designates whether this account should be treated as active. "
|
||||||
"Unselect this instead of deleting accounts."))
|
"Unselect this instead of deleting accounts."))
|
||||||
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
|
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
|
||||||
|
|
||||||
objects = AccountManager()
|
objects = AccountManager()
|
||||||
|
|
||||||
USERNAME_FIELD = 'username'
|
USERNAME_FIELD = 'username'
|
||||||
REQUIRED_FIELDS = ['email']
|
REQUIRED_FIELDS = ['email']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# ignore `is_staff` kwarg because is handled with `is_superuser`
|
||||||
|
kwargs.pop('is_staff', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_staff(self):
|
def is_staff(self):
|
||||||
return self.is_superuser
|
return self.is_superuser
|
||||||
|
|
||||||
def save(self, active_systemuser=False, *args, **kwargs):
|
def save(self, active_systemuser=False, *args, **kwargs):
|
||||||
created = not self.pk
|
created = not self.pk
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -75,21 +80,21 @@ class Account(auth.AbstractBaseUser):
|
||||||
self.save(update_fields=('main_systemuser',))
|
self.save(update_fields=('main_systemuser',))
|
||||||
elif was_active != self.is_active:
|
elif was_active != self.is_active:
|
||||||
self.notify_related()
|
self.notify_related()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.short_name = self.short_name.strip()
|
self.short_name = self.short_name.strip()
|
||||||
self.full_name = self.full_name.strip()
|
self.full_name = self.full_name.strip()
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
self.notify_related()
|
self.notify_related()
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
self.notify_related()
|
self.notify_related()
|
||||||
|
|
||||||
def get_services_to_disable(self):
|
def get_services_to_disable(self):
|
||||||
related_fields = [
|
related_fields = [
|
||||||
f for f in self._meta.get_fields()
|
f for f in self._meta.get_fields()
|
||||||
|
@ -101,20 +106,20 @@ class Account(auth.AbstractBaseUser):
|
||||||
if source in core.services and hasattr(source, 'active'):
|
if source in core.services and hasattr(source, 'active'):
|
||||||
for obj in getattr(self, rel.get_accessor_name()).all():
|
for obj in getattr(self, rel.get_accessor_name()).all():
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
def notify_related(self):
|
def notify_related(self):
|
||||||
""" Trigger save() on related objects that depend on this account """
|
""" Trigger save() on related objects that depend on this account """
|
||||||
for obj in self.get_services_to_disable():
|
for obj in self.get_services_to_disable():
|
||||||
signals.pre_save.send(sender=type(obj), instance=obj)
|
signals.pre_save.send(sender=type(obj), instance=obj)
|
||||||
signals.post_save.send(sender=type(obj), instance=obj)
|
signals.post_save.send(sender=type(obj), instance=obj)
|
||||||
# OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=())
|
# OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=())
|
||||||
|
|
||||||
def get_contacts_emails(self, usages=None):
|
def get_contacts_emails(self, usages=None):
|
||||||
contacts = self.contacts.all()
|
contacts = self.contacts.all()
|
||||||
if usages is not None:
|
if usages is not None:
|
||||||
contactes = contacts.filter(email_usages=usages)
|
contactes = contacts.filter(email_usages=usages)
|
||||||
return contacts.values_list('email', flat=True)
|
return contacts.values_list('email', flat=True)
|
||||||
|
|
||||||
def send_email(self, template, context, email_from=None, usages=None, attachments=[], html=None):
|
def send_email(self, template, context, email_from=None, usages=None, attachments=[], html=None):
|
||||||
contacts = self.contacts.filter(email_usages=usages)
|
contacts = self.contacts.filter(email_usages=usages)
|
||||||
email_to = self.get_contacts_emails(usages)
|
email_to = self.get_contacts_emails(usages)
|
||||||
|
@ -126,14 +131,14 @@ class Account(auth.AbstractBaseUser):
|
||||||
with translation.override(self.language):
|
with translation.override(self.language):
|
||||||
send_email_template(template, extra_context, email_to, email_from=email_from,
|
send_email_template(template, extra_context, email_to, email_from=email_from,
|
||||||
html=html, attachments=attachments)
|
html=html, attachments=attachments)
|
||||||
|
|
||||||
def get_full_name(self):
|
def get_full_name(self):
|
||||||
return self.full_name or self.short_name or self.username
|
return self.full_name or self.short_name or self.username
|
||||||
|
|
||||||
def get_short_name(self):
|
def get_short_name(self):
|
||||||
""" Returns the short name for the user """
|
""" Returns the short name for the user """
|
||||||
return self.short_name or self.username or self.full_name
|
return self.short_name or self.username or self.full_name
|
||||||
|
|
||||||
def has_perm(self, perm, obj=None):
|
def has_perm(self, perm, obj=None):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has the specified permission. This method
|
Returns True if the user has the specified permission. This method
|
||||||
|
@ -160,7 +165,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
elif obj and getattr(obj, 'account', None) == self:
|
elif obj and getattr(obj, 'account', None) == self:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def has_perms(self, perm_list, obj=None):
|
def has_perms(self, perm_list, obj=None):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has each of the specified permissions. If
|
Returns True if the user has each of the specified permissions. If
|
||||||
|
@ -171,7 +176,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
if not self.has_perm(perm, obj):
|
if not self.has_perm(perm, obj):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def has_module_perms(self, app_label):
|
def has_module_perms(self, app_label):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has any permissions in the given app label.
|
Returns True if the user has any permissions in the given app label.
|
||||||
|
|
|
@ -5,7 +5,7 @@ from datetime import date
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.forms.models import modelformset_factory
|
from django.forms.models import modelformset_factory
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
@ -179,7 +179,7 @@ def undo_billing(modeladmin, request, queryset):
|
||||||
group[line.order].append(line)
|
group[line.order].append(line)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
group[line.order] = [line]
|
group[line.order] = [line]
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
for order, lines in group.items():
|
for order, lines in group.items():
|
||||||
prev = None
|
prev = None
|
||||||
|
@ -211,7 +211,7 @@ def undo_billing(modeladmin, request, queryset):
|
||||||
messages.error(request, "Order does not have lines!.")
|
messages.error(request, "Order does not have lines!.")
|
||||||
order.billed_until = billed_until
|
order.billed_until = billed_until
|
||||||
order.billed_on = billed_on
|
order.billed_on = billed_on
|
||||||
|
|
||||||
# Commit changes
|
# Commit changes
|
||||||
norders, nlines = 0, 0
|
norders, nlines = 0, 0
|
||||||
for order, lines in group.items():
|
for order, lines in group.items():
|
||||||
|
@ -221,7 +221,7 @@ def undo_billing(modeladmin, request, queryset):
|
||||||
# TODO update order history undo billing
|
# TODO update order history undo billing
|
||||||
order.save(update_fields=('billed_until', 'billed_on'))
|
order.save(update_fields=('billed_until', 'billed_on'))
|
||||||
norders += 1
|
norders += 1
|
||||||
|
|
||||||
messages.success(request, _("%(norders)s orders and %(nlines)s lines undoed.") % {
|
messages.success(request, _("%(norders)s orders and %(nlines)s lines undoed.") % {
|
||||||
'nlines': nlines,
|
'nlines': nlines,
|
||||||
'norders': norders
|
'norders': norders
|
||||||
|
|
|
@ -2,11 +2,12 @@ from django import forms
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Sum, Prefetch
|
from django.db.models import F, Sum, Prefetch
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -15,7 +16,7 @@ from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
|
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from . import settings, actions
|
from . import settings, actions
|
||||||
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
||||||
|
@ -39,18 +40,18 @@ PAYMENT_STATE_COLORS = {
|
||||||
class BillSublineInline(admin.TabularInline):
|
class BillSublineInline(admin.TabularInline):
|
||||||
model = BillSubline
|
model = BillSubline
|
||||||
fields = ('description', 'total', 'type')
|
fields = ('description', 'total', 'type')
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = super().get_readonly_fields(request, obj)
|
fields = super().get_readonly_fields(request, obj)
|
||||||
if obj and not obj.bill.is_open:
|
if obj and not obj.bill.is_open:
|
||||||
return self.get_fields(request)
|
return self.get_fields(request)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_max_num(self, request, obj=None):
|
def get_max_num(self, request, obj=None):
|
||||||
if obj and not obj.bill.is_open:
|
if obj and not obj.bill.is_open:
|
||||||
return 0
|
return 0
|
||||||
return super().get_max_num(request, obj)
|
return super().get_max_num(request, obj)
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
if obj and not obj.bill.is_open:
|
if obj and not obj.bill.is_open:
|
||||||
return False
|
return False
|
||||||
|
@ -64,9 +65,10 @@ class BillLineInline(admin.TabularInline):
|
||||||
'subtotal', 'display_total',
|
'subtotal', 'display_total',
|
||||||
)
|
)
|
||||||
readonly_fields = ('display_total', 'order_link')
|
readonly_fields = ('display_total', 'order_link')
|
||||||
|
|
||||||
order_link = admin_link('order', display='pk')
|
order_link = admin_link('order', display='pk')
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_total(self, line):
|
def display_total(self, line):
|
||||||
if line.pk:
|
if line.pk:
|
||||||
total = line.compute_total()
|
total = line.compute_total()
|
||||||
|
@ -78,8 +80,7 @@ class BillLineInline(admin.TabularInline):
|
||||||
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
|
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
|
||||||
return '<a href="%s">%s</a>' % (url, total)
|
return '<a href="%s">%s</a>' % (url, total)
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.allow_tags = True
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'description':
|
if db_field.name == 'description':
|
||||||
|
@ -87,7 +88,7 @@ class BillLineInline(admin.TabularInline):
|
||||||
elif db_field.name not in ('start_on', 'end_on'):
|
elif db_field.name not in ('start_on', 'end_on'):
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'6'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'6'})
|
||||||
return super().formfield_for_dbfield(db_field, **kwargs)
|
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.prefetch_related('sublines').select_related('order')
|
return qs.prefetch_related('sublines').select_related('order')
|
||||||
|
@ -96,36 +97,35 @@ class BillLineInline(admin.TabularInline):
|
||||||
class ClosedBillLineInline(BillLineInline):
|
class ClosedBillLineInline(BillLineInline):
|
||||||
# TODO reimplement as nested inlines when upstream
|
# TODO reimplement as nested inlines when upstream
|
||||||
# https://code.djangoproject.com/ticket/9025
|
# https://code.djangoproject.com/ticket/9025
|
||||||
|
|
||||||
fields = (
|
fields = (
|
||||||
'display_description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
|
'display_description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
|
||||||
'display_subtotal', 'display_total'
|
'display_subtotal', 'display_total'
|
||||||
)
|
)
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
can_delete = False
|
can_delete = False
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_description(self, line):
|
def display_description(self, line):
|
||||||
descriptions = [line.description]
|
descriptions = [line.description]
|
||||||
for subline in line.sublines.all():
|
for subline in line.sublines.all():
|
||||||
descriptions.append(' '*4+subline.description)
|
descriptions.append(' ' * 4 + subline.description)
|
||||||
return '<br>'.join(descriptions)
|
return '<br>'.join(descriptions)
|
||||||
display_description.short_description = _("Description")
|
display_description.short_description = _("Description")
|
||||||
display_description.allow_tags = True
|
|
||||||
|
@mark_safe
|
||||||
def display_subtotal(self, line):
|
def display_subtotal(self, line):
|
||||||
subtotals = [' ' + str(line.subtotal)]
|
subtotals = [' ' + str(line.subtotal)]
|
||||||
for subline in line.sublines.all():
|
for subline in line.sublines.all():
|
||||||
subtotals.append(str(subline.total))
|
subtotals.append(str(subline.total))
|
||||||
return '<br>'.join(subtotals)
|
return '<br>'.join(subtotals)
|
||||||
display_subtotal.short_description = _("Subtotal")
|
display_subtotal.short_description = _("Subtotal")
|
||||||
display_subtotal.allow_tags = True
|
|
||||||
|
|
||||||
def display_total(self, line):
|
def display_total(self, line):
|
||||||
if line.pk:
|
if line.pk:
|
||||||
return line.compute_total()
|
return line.compute_total()
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.allow_tags = True
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -158,28 +158,28 @@ class BillLineAdmin(admin.ModelAdmin):
|
||||||
list_select_related = ('bill', 'bill__account')
|
list_select_related = ('bill', 'bill__account')
|
||||||
search_fields = ('description', 'bill__number')
|
search_fields = ('description', 'bill__number')
|
||||||
inlines = (BillSublineInline,)
|
inlines = (BillSublineInline,)
|
||||||
|
|
||||||
account_link = admin_link('bill__account')
|
account_link = admin_link('bill__account')
|
||||||
bill_link = admin_link('bill')
|
bill_link = admin_link('bill')
|
||||||
order_link = admin_link('order')
|
order_link = admin_link('order')
|
||||||
amended_line_link = admin_link('amended_line')
|
amended_line_link = admin_link('amended_line')
|
||||||
|
|
||||||
def display_is_open(self, instance):
|
def display_is_open(self, instance):
|
||||||
return instance.bill.is_open
|
return instance.bill.is_open
|
||||||
display_is_open.short_description = _("Is open")
|
display_is_open.short_description = _("Is open")
|
||||||
display_is_open.boolean = True
|
display_is_open.boolean = True
|
||||||
|
|
||||||
def display_sublinetotal(self, instance):
|
def display_sublinetotal(self, instance):
|
||||||
total = instance.subline_total
|
total = instance.subline_total
|
||||||
return total if total is not None else '---'
|
return total if total is not None else '---'
|
||||||
display_sublinetotal.short_description = _("Sublines")
|
display_sublinetotal.short_description = _("Sublines")
|
||||||
display_sublinetotal.admin_order_field = 'subline_total'
|
display_sublinetotal.admin_order_field = 'subline_total'
|
||||||
|
|
||||||
def display_total(self, instance):
|
def display_total(self, instance):
|
||||||
return round(instance.computed_total or 0, 2)
|
return round(instance.computed_total or 0, 2)
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.admin_order_field = 'computed_total'
|
display_total.admin_order_field = 'computed_total'
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = super().get_readonly_fields(request, obj)
|
fields = super().get_readonly_fields(request, obj)
|
||||||
if obj and not obj.bill.is_open:
|
if obj and not obj.bill.is_open:
|
||||||
|
@ -188,7 +188,7 @@ class BillLineAdmin(admin.ModelAdmin):
|
||||||
'subtotal', 'order_billed_on', 'order_billed_until'
|
'subtotal', 'order_billed_on', 'order_billed_until'
|
||||||
]
|
]
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
qs = qs.annotate(
|
qs = qs.annotate(
|
||||||
|
@ -196,7 +196,7 @@ class BillLineAdmin(admin.ModelAdmin):
|
||||||
computed_total=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100),
|
computed_total=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100),
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
if obj and not obj.bill.is_open:
|
if obj and not obj.bill.is_open:
|
||||||
return False
|
return False
|
||||||
|
@ -209,7 +209,7 @@ class BillLineManagerAdmin(BillLineAdmin):
|
||||||
if self.bill_ids:
|
if self.bill_ids:
|
||||||
return qset.filter(bill_id__in=self.bill_ids)
|
return qset.filter(bill_id__in=self.bill_ids)
|
||||||
return qset
|
return qset
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
GET_copy = request.GET.copy()
|
GET_copy = request.GET.copy()
|
||||||
bill_ids = GET_copy.pop('ids', None)
|
bill_ids = GET_copy.pop('ids', None)
|
||||||
|
@ -242,6 +242,7 @@ class BillLineManagerAdmin(BillLineAdmin):
|
||||||
|
|
||||||
|
|
||||||
class BillAdminMixin(AccountAdminMixin):
|
class BillAdminMixin(AccountAdminMixin):
|
||||||
|
@mark_safe
|
||||||
def display_total_with_subtotals(self, bill):
|
def display_total_with_subtotals(self, bill):
|
||||||
if bill.pk:
|
if bill.pk:
|
||||||
currency = settings.BILLS_CURRENCY.lower()
|
currency = settings.BILLS_CURRENCY.lower()
|
||||||
|
@ -251,10 +252,10 @@ class BillAdminMixin(AccountAdminMixin):
|
||||||
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
||||||
subtotals = '\n'.join(subtotals)
|
subtotals = '\n'.join(subtotals)
|
||||||
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
|
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
|
||||||
display_total_with_subtotals.allow_tags = True
|
|
||||||
display_total_with_subtotals.short_description = _("total")
|
display_total_with_subtotals.short_description = _("total")
|
||||||
display_total_with_subtotals.admin_order_field = 'approx_total'
|
display_total_with_subtotals.admin_order_field = 'approx_total'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_payment_state(self, bill):
|
def display_payment_state(self, bill):
|
||||||
if bill.pk:
|
if bill.pk:
|
||||||
t_opts = bill.transactions.model._meta
|
t_opts = bill.transactions.model._meta
|
||||||
|
@ -276,7 +277,6 @@ class BillAdminMixin(AccountAdminMixin):
|
||||||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||||
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
||||||
url=url, color=color, name=state, title=title)
|
url=url, color=color, name=state, title=title)
|
||||||
display_payment_state.allow_tags = True
|
|
||||||
display_payment_state.short_description = _("Payment")
|
display_payment_state.short_description = _("Payment")
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
@ -304,9 +304,9 @@ class AmendInline(BillAdminMixin, admin.TabularInline):
|
||||||
verbose_name_plural = _("Amends")
|
verbose_name_plural = _("Amends")
|
||||||
can_delete = False
|
can_delete = False
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
self_link = admin_link('__str__')
|
self_link = admin_link('__str__')
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -354,12 +354,12 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
'closed_on_display', 'updated_on_display', 'display_total_with_subtotals',
|
'closed_on_display', 'updated_on_display', 'display_total_with_subtotals',
|
||||||
)
|
)
|
||||||
date_hierarchy = 'closed_on'
|
date_hierarchy = 'closed_on'
|
||||||
|
|
||||||
created_on_display = admin_date('created_on', short_description=_("Created"))
|
created_on_display = admin_date('created_on', short_description=_("Created"))
|
||||||
closed_on_display = admin_date('closed_on', short_description=_("Closed"))
|
closed_on_display = admin_date('closed_on', short_description=_("Closed"))
|
||||||
updated_on_display = admin_date('updated_on', short_description=_("Updated"))
|
updated_on_display = admin_date('updated_on', short_description=_("Updated"))
|
||||||
amend_of_link = admin_link('amend_of')
|
amend_of_link = admin_link('amend_of')
|
||||||
|
|
||||||
# def amend_links(self, bill):
|
# def amend_links(self, bill):
|
||||||
# links = []
|
# links = []
|
||||||
# for amend in bill.amends.all():
|
# for amend in bill.amends.all():
|
||||||
|
@ -368,27 +368,25 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
# return '<br>'.join(links)
|
# return '<br>'.join(links)
|
||||||
# amend_links.short_description = _("Amends")
|
# amend_links.short_description = _("Amends")
|
||||||
# amend_links.allow_tags = True
|
# amend_links.allow_tags = True
|
||||||
|
|
||||||
def num_lines(self, bill):
|
def num_lines(self, bill):
|
||||||
return bill.lines__count
|
return bill.lines__count
|
||||||
num_lines.admin_order_field = 'lines__count'
|
num_lines.admin_order_field = 'lines__count'
|
||||||
num_lines.short_description = _("lines")
|
num_lines.short_description = _("lines")
|
||||||
|
|
||||||
def display_total(self, bill):
|
def display_total(self, bill):
|
||||||
currency = settings.BILLS_CURRENCY.lower()
|
currency = settings.BILLS_CURRENCY.lower()
|
||||||
return '%s &%s;' % (bill.compute_total(), currency)
|
return format_html('{} &{};', bill.compute_total(), currency)
|
||||||
display_total.allow_tags = True
|
|
||||||
display_total.short_description = _("total")
|
display_total.short_description = _("total")
|
||||||
display_total.admin_order_field = 'approx_total'
|
display_total.admin_order_field = 'approx_total'
|
||||||
|
|
||||||
def type_link(self, bill):
|
def type_link(self, bill):
|
||||||
bill_type = bill.type.lower()
|
bill_type = bill.type.lower()
|
||||||
url = reverse('admin:bills_%s_changelist' % bill_type)
|
url = reverse('admin:bills_%s_changelist' % bill_type)
|
||||||
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
|
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
|
||||||
type_link.allow_tags = True
|
|
||||||
type_link.short_description = _("type")
|
type_link.short_description = _("type")
|
||||||
type_link.admin_order_field = 'type'
|
type_link.admin_order_field = 'type'
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
""" Hook bill lines management URLs on bill admin """
|
""" Hook bill lines management URLs on bill admin """
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
|
@ -399,13 +397,13 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
name='bills_bill_manage_lines'),
|
name='bills_bill_manage_lines'),
|
||||||
]
|
]
|
||||||
return extra_urls + urls
|
return extra_urls + urls
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = super().get_readonly_fields(request, obj)
|
fields = super().get_readonly_fields(request, obj)
|
||||||
if obj and not obj.is_open:
|
if obj and not obj.is_open:
|
||||||
fields += self.add_fields
|
fields += self.add_fields
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super().get_fieldsets(request, obj)
|
fieldsets = super().get_fieldsets(request, obj)
|
||||||
if obj:
|
if obj:
|
||||||
|
@ -418,7 +416,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
if obj.is_open:
|
if obj.is_open:
|
||||||
fieldsets = fieldsets[0:-1]
|
fieldsets = fieldsets[0:-1]
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def get_change_view_actions(self, obj=None):
|
def get_change_view_actions(self, obj=None):
|
||||||
actions = super().get_change_view_actions(obj)
|
actions = super().get_change_view_actions(obj)
|
||||||
exclude = []
|
exclude = []
|
||||||
|
@ -428,7 +426,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
if obj.type not in obj.AMEND_MAP:
|
if obj.type not in obj.AMEND_MAP:
|
||||||
exclude += ['amend_bills']
|
exclude += ['amend_bills']
|
||||||
return [action for action in actions if action.__name__ not in exclude]
|
return [action for action in actions if action.__name__ not in exclude]
|
||||||
|
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if obj and not obj.is_open:
|
if obj and not obj.is_open:
|
||||||
|
@ -439,7 +437,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
else:
|
else:
|
||||||
cls.inlines = [BillLineInline]
|
cls.inlines = [BillLineInline]
|
||||||
return super().get_inline_instances(request, obj)
|
return super().get_inline_instances(request, obj)
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'comments':
|
if db_field.name == 'comments':
|
||||||
|
@ -450,7 +448,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
if db_field.name == 'amend_of':
|
if db_field.name == 'amend_of':
|
||||||
formfield.queryset = formfield.queryset.filter(is_open=False)
|
formfield.queryset = formfield.queryset.filter(is_open=False)
|
||||||
return formfield
|
return formfield
|
||||||
|
|
||||||
def change_view(self, request, object_id, **kwargs):
|
def change_view(self, request, object_id, **kwargs):
|
||||||
# TODO raise404, here and everywhere
|
# TODO raise404, here and everywhere
|
||||||
bill = self.get_object(request, unquote(object_id))
|
bill = self.get_object(request, unquote(object_id))
|
||||||
|
@ -471,7 +469,7 @@ admin.site.register(BillLine, BillLineAdmin)
|
||||||
class BillContactInline(admin.StackedInline):
|
class BillContactInline(admin.StackedInline):
|
||||||
model = BillContact
|
model = BillContact
|
||||||
fields = ('name', 'address', ('city', 'zipcode'), 'country', 'vat')
|
fields = ('name', 'address', ('city', 'zipcode'), 'country', 'vat')
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'name':
|
if db_field.name == 'name':
|
||||||
|
@ -479,7 +477,7 @@ class BillContactInline(admin.StackedInline):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||||
return super().formfield_for_dbfield(db_field, **kwargs)
|
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from orchestra.api import router, LogApiMixin
|
from orchestra.api import router, LogApiMixin
|
||||||
from orchestra.contrib.accounts.api import AccountApiMixin
|
from orchestra.contrib.accounts.api import AccountApiMixin
|
||||||
|
@ -14,8 +14,8 @@ from .serializers import BillSerializer
|
||||||
class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Bill.objects.all()
|
queryset = Bill.objects.all()
|
||||||
serializer_class = BillSerializer
|
serializer_class = BillSerializer
|
||||||
|
|
||||||
@detail_route(methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def document(self, request, pk):
|
def document(self, request, pk):
|
||||||
bill = self.get_object()
|
bill = self.get_object()
|
||||||
content_type = request.META.get('HTTP_ACCEPT')
|
content_type = request.META.get('HTTP_ACCEPT')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib.admin import SimpleListFilter
|
from django.contrib.admin import SimpleListFilter
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -11,11 +11,11 @@ class BillTypeListFilter(SimpleListFilter):
|
||||||
""" Filter tickets by created_by according to request.user """
|
""" Filter tickets by created_by according to request.user """
|
||||||
title = 'Type'
|
title = 'Type'
|
||||||
parameter_name = ''
|
parameter_name = ''
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
super(BillTypeListFilter, self).__init__(request, *args, **kwargs)
|
super(BillTypeListFilter, self).__init__(request, *args, **kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return (
|
return (
|
||||||
('bill', _("All")),
|
('bill', _("All")),
|
||||||
|
@ -25,13 +25,13 @@ class BillTypeListFilter(SimpleListFilter):
|
||||||
('amendmentfee', _("Amendment fee")),
|
('amendmentfee', _("Amendment fee")),
|
||||||
('amendmentinvoice', _("Amendment invoice")),
|
('amendmentinvoice', _("Amendment invoice")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def value(self):
|
def value(self):
|
||||||
return self.request.path.split('/')[-2]
|
return self.request.path.split('/')[-2]
|
||||||
|
|
||||||
def choices(self, cl):
|
def choices(self, cl):
|
||||||
query = self.request.GET.urlencode()
|
query = self.request.GET.urlencode()
|
||||||
for lookup, title in self.lookup_choices:
|
for lookup, title in self.lookup_choices:
|
||||||
|
@ -45,7 +45,7 @@ class BillTypeListFilter(SimpleListFilter):
|
||||||
class TotalListFilter(SimpleListFilter):
|
class TotalListFilter(SimpleListFilter):
|
||||||
title = _("total")
|
title = _("total")
|
||||||
parameter_name = 'total'
|
parameter_name = 'total'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return (
|
return (
|
||||||
('gt', mark_safe("total > 0")),
|
('gt', mark_safe("total > 0")),
|
||||||
|
@ -53,7 +53,7 @@ class TotalListFilter(SimpleListFilter):
|
||||||
('eq', "total = 0"),
|
('eq', "total = 0"),
|
||||||
('ne', mark_safe("total ≠ 0")),
|
('ne', mark_safe("total ≠ 0")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == 'gt':
|
if self.value() == 'gt':
|
||||||
return queryset.filter(approx_total__gt=0)
|
return queryset.filter(approx_total__gt=0)
|
||||||
|
@ -70,13 +70,13 @@ class HasBillContactListFilter(SimpleListFilter):
|
||||||
""" Filter Nodes by group according to request.user """
|
""" Filter Nodes by group according to request.user """
|
||||||
title = _("has bill contact")
|
title = _("has bill contact")
|
||||||
parameter_name = 'bill'
|
parameter_name = 'bill'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return (
|
return (
|
||||||
('True', _("Yes")),
|
('True', _("Yes")),
|
||||||
('False', _("No")),
|
('False', _("No")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == 'True':
|
if self.value() == 'True':
|
||||||
return queryset.filter(billcontact__isnull=False)
|
return queryset.filter(billcontact__isnull=False)
|
||||||
|
@ -87,7 +87,7 @@ class HasBillContactListFilter(SimpleListFilter):
|
||||||
class PaymentStateListFilter(SimpleListFilter):
|
class PaymentStateListFilter(SimpleListFilter):
|
||||||
title = _("payment state")
|
title = _("payment state")
|
||||||
parameter_name = 'payment_state'
|
parameter_name = 'payment_state'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return (
|
return (
|
||||||
('OPEN', _("Open")),
|
('OPEN', _("Open")),
|
||||||
|
@ -95,7 +95,7 @@ class PaymentStateListFilter(SimpleListFilter):
|
||||||
('PENDING', _("Pending")),
|
('PENDING', _("Pending")),
|
||||||
('BAD_DEBT', _("Bad debt")),
|
('BAD_DEBT', _("Bad debt")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
# FIXME use queryset.computed_total instead of approx_total, bills.admin.BillAdmin.get_queryset
|
# FIXME use queryset.computed_total instead of approx_total, bills.admin.BillAdmin.get_queryset
|
||||||
Transaction = queryset.model.transactions.field.remote_field.related_model
|
Transaction = queryset.model.transactions.field.remote_field.related_model
|
||||||
|
@ -137,7 +137,7 @@ class PaymentStateListFilter(SimpleListFilter):
|
||||||
class AmendedListFilter(SimpleListFilter):
|
class AmendedListFilter(SimpleListFilter):
|
||||||
title = _("amended")
|
title = _("amended")
|
||||||
parameter_name = 'amended'
|
parameter_name = 'amended'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return (
|
return (
|
||||||
('3', _("Closed amends")),
|
('3', _("Closed amends")),
|
||||||
|
@ -145,7 +145,7 @@ class AmendedListFilter(SimpleListFilter):
|
||||||
('1', _("Any amends")),
|
('1', _("Any amends")),
|
||||||
('0', _("No amends")),
|
('0', _("No amends")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() is None:
|
if self.value() is None:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -21,7 +21,7 @@ def validate_contact(request, bill, error=True):
|
||||||
message = msg.format(relation=_("Related"), account=account, url=url)
|
message = msg.format(relation=_("Related"), account=account, url=url)
|
||||||
send(request, mark_safe(message))
|
send(request, mark_safe(message))
|
||||||
valid = False
|
valid = False
|
||||||
main = type(bill).account.field.rel.to.objects.get_main()
|
main = type(bill).account.field.related_model.objects.get_main()
|
||||||
if not hasattr(main, 'billcontact'):
|
if not hasattr(main, 'billcontact'):
|
||||||
account = force_text(main)
|
account = force_text(main)
|
||||||
url = reverse('admin:accounts_account_change', args=(main.id,))
|
url = reverse('admin:accounts_account_change', args=(main.id,))
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bill',
|
model_name='bill',
|
||||||
name='amend_of',
|
name='amend_of',
|
||||||
field=models.ForeignKey(to='bills.Bill', blank=True, related_name='amends', verbose_name='amend of', null=True),
|
field=models.ForeignKey(to='bills.Bill', blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='amends', verbose_name='amend of', null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='billcontact',
|
model_name='billcontact',
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,12 @@
|
||||||
import datetime
|
import datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.validators import ValidationError, RegexValidator
|
from django.core.validators import ValidationError, RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Sum
|
from django.db.models import F, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.template import loader, Context
|
from django.template import loader
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
@ -24,7 +24,7 @@ from . import settings
|
||||||
|
|
||||||
class BillContact(models.Model):
|
class BillContact(models.Model):
|
||||||
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
|
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='billcontact')
|
related_name='billcontact', on_delete=models.CASCADE)
|
||||||
name = models.CharField(_("name"), max_length=256, blank=True,
|
name = models.CharField(_("name"), max_length=256, blank=True,
|
||||||
help_text=_("Account full name will be used when left blank."))
|
help_text=_("Account full name will be used when left blank."))
|
||||||
address = models.TextField(_("address"))
|
address = models.TextField(_("address"))
|
||||||
|
@ -36,13 +36,13 @@ class BillContact(models.Model):
|
||||||
choices=settings.BILLS_CONTACT_COUNTRIES,
|
choices=settings.BILLS_CONTACT_COUNTRIES,
|
||||||
default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)
|
default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)
|
||||||
vat = models.CharField(_("VAT number"), max_length=64)
|
vat = models.CharField(_("VAT number"), max_length=64)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return self.name or self.account.get_full_name()
|
return self.name or self.account.get_full_name()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.vat = self.vat.strip()
|
self.vat = self.vat.strip()
|
||||||
self.city = self.city.strip()
|
self.city = self.city.strip()
|
||||||
|
@ -99,12 +99,12 @@ class Bill(models.Model):
|
||||||
INVOICE: AMENDMENTINVOICE,
|
INVOICE: AMENDMENTINVOICE,
|
||||||
FEE: AMENDMENTFEE,
|
FEE: AMENDMENTFEE,
|
||||||
}
|
}
|
||||||
|
|
||||||
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='%(class)s')
|
related_name='%(class)s', on_delete=models.CASCADE)
|
||||||
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
|
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
|
||||||
related_name='amends')
|
related_name='amends', on_delete=models.SET_NULL)
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||||
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
||||||
|
@ -115,37 +115,37 @@ class Bill(models.Model):
|
||||||
# total = models.DecimalField(max_digits=12, decimal_places=2, null=True)
|
# total = models.DecimalField(max_digits=12, decimal_places=2, null=True)
|
||||||
comments = models.TextField(_("comments"), blank=True)
|
comments = models.TextField(_("comments"), blank=True)
|
||||||
html = models.TextField(_("HTML"), blank=True)
|
html = models.TextField(_("HTML"), blank=True)
|
||||||
|
|
||||||
objects = BillManager()
|
objects = BillManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'id'
|
get_latest_by = 'id'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.number
|
return self.number
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_class_type(cls):
|
def get_class_type(cls):
|
||||||
if cls is models.DEFERRED:
|
if cls is models.DEFERRED:
|
||||||
cls = cls.__base__
|
cls = cls.__base__
|
||||||
return cls.__name__.upper()
|
return cls.__name__.upper()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def total(self):
|
def total(self):
|
||||||
return self.compute_total()
|
return self.compute_total()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def seller(self):
|
def seller(self):
|
||||||
return Account.objects.get_main().billcontact
|
return Account.objects.get_main().billcontact
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def buyer(self):
|
def buyer(self):
|
||||||
return self.account.billcontact
|
return self.account.billcontact
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_multiple_pages(self):
|
def has_multiple_pages(self):
|
||||||
return self.type != self.FEE
|
return self.type != self.FEE
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def payment_state(self):
|
def payment_state(self):
|
||||||
if self.is_open or self.get_type() == self.PROFORMA:
|
if self.is_open or self.get_type() == self.PROFORMA:
|
||||||
|
@ -192,7 +192,7 @@ class Bill(models.Model):
|
||||||
elif executed:
|
elif executed:
|
||||||
return self.EXECUTED
|
return self.EXECUTED
|
||||||
return self.BAD_DEBT
|
return self.BAD_DEBT
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.amend_of_id:
|
if self.amend_of_id:
|
||||||
errors = {}
|
errors = {}
|
||||||
|
@ -206,27 +206,27 @@ class Bill(models.Model):
|
||||||
errors['amend_of'] = _("Related invoice is an amendment.")
|
errors['amend_of'] = _("Related invoice is an amendment.")
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
def get_payment_state_display(self):
|
def get_payment_state_display(self):
|
||||||
value = self.payment_state
|
value = self.payment_state
|
||||||
return force_text(dict(self.PAYMENT_STATES).get(value, value))
|
return force_text(dict(self.PAYMENT_STATES).get(value, value))
|
||||||
|
|
||||||
def get_current_transaction(self):
|
def get_current_transaction(self):
|
||||||
return self.transactions.exclude_rejected().first()
|
return self.transactions.exclude_rejected().first()
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
return self.type or self.get_class_type()
|
return self.type or self.get_class_type()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_amend(self):
|
def is_amend(self):
|
||||||
return self.type in self.AMEND_MAP.values()
|
return self.type in self.AMEND_MAP.values()
|
||||||
|
|
||||||
def get_amend_type(self):
|
def get_amend_type(self):
|
||||||
amend_type = self.AMEND_MAP.get(self.type)
|
amend_type = self.AMEND_MAP.get(self.type)
|
||||||
if amend_type is None:
|
if amend_type is None:
|
||||||
raise TypeError("%s has no associated amend type." % self.type)
|
raise TypeError("%s has no associated amend type." % self.type)
|
||||||
return amend_type
|
return amend_type
|
||||||
|
|
||||||
def get_number(self):
|
def get_number(self):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if cls is models.DEFERRED:
|
if cls is models.DEFERRED:
|
||||||
|
@ -250,16 +250,16 @@ class Bill(models.Model):
|
||||||
zeros = (number_length - len(str(number))) * '0'
|
zeros = (number_length - len(str(number))) * '0'
|
||||||
number = zeros + str(number)
|
number = zeros + str(number)
|
||||||
return '{prefix}{year}{number}'.format(prefix=prefix, year=year, number=number)
|
return '{prefix}{year}{number}'.format(prefix=prefix, year=year, number=number)
|
||||||
|
|
||||||
def get_due_date(self, payment=None):
|
def get_due_date(self, payment=None):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
if payment:
|
if payment:
|
||||||
return now + payment.get_due_delta()
|
return now + payment.get_due_delta()
|
||||||
return now + relativedelta(months=1)
|
return now + relativedelta(months=1)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('admin:bills_bill_view', args=(self.pk,))
|
return reverse('admin:bills_bill_view', args=(self.pk,))
|
||||||
|
|
||||||
def close(self, payment=False):
|
def close(self, payment=False):
|
||||||
if not self.is_open:
|
if not self.is_open:
|
||||||
raise TypeError("Bill not in Open state.")
|
raise TypeError("Bill not in Open state.")
|
||||||
|
@ -278,10 +278,10 @@ class Bill(models.Model):
|
||||||
self.html = self.render(payment=payment)
|
self.html = self.render(payment=payment)
|
||||||
self.save()
|
self.save()
|
||||||
return transaction
|
return transaction
|
||||||
|
|
||||||
def get_billing_contact_emails(self):
|
def get_billing_contact_emails(self):
|
||||||
return self.account.get_contacts_emails(usages=(Contact.BILLING,))
|
return self.account.get_contacts_emails(usages=(Contact.BILLING,))
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
pdf = self.as_pdf()
|
pdf = self.as_pdf()
|
||||||
self.account.send_email(
|
self.account.send_email(
|
||||||
|
@ -298,12 +298,12 @@ class Bill(models.Model):
|
||||||
)
|
)
|
||||||
self.is_sent = True
|
self.is_sent = True
|
||||||
self.save(update_fields=['is_sent'])
|
self.save(update_fields=['is_sent'])
|
||||||
|
|
||||||
def render(self, payment=False, language=None):
|
def render(self, payment=False, language=None):
|
||||||
with translation.override(language or self.account.language):
|
with translation.override(language or self.account.language):
|
||||||
if payment is False:
|
if payment is False:
|
||||||
payment = self.account.paymentsources.get_default()
|
payment = self.account.paymentsources.get_default()
|
||||||
context = Context({
|
context = {
|
||||||
'bill': self,
|
'bill': self,
|
||||||
'lines': self.lines.all().prefetch_related('sublines'),
|
'lines': self.lines.all().prefetch_related('sublines'),
|
||||||
'seller': self.seller,
|
'seller': self.seller,
|
||||||
|
@ -318,29 +318,29 @@ class Bill(models.Model):
|
||||||
'payment': payment and payment.get_bill_context(),
|
'payment': payment and payment.get_bill_context(),
|
||||||
'default_due_date': self.get_due_date(payment=payment),
|
'default_due_date': self.get_due_date(payment=payment),
|
||||||
'now': timezone.now(),
|
'now': timezone.now(),
|
||||||
})
|
}
|
||||||
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
||||||
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
||||||
bill_template = loader.get_template(template)
|
bill_template = loader.get_template(template)
|
||||||
html = bill_template.render(context)
|
html = bill_template.render(context)
|
||||||
html = html.replace('-pageskip-', '<pdf:nextpage />')
|
html = html.replace('-pageskip-', '<pdf:nextpage />')
|
||||||
return html
|
return html
|
||||||
|
|
||||||
def as_pdf(self):
|
def as_pdf(self):
|
||||||
html = self.html or self.render()
|
html = self.html or self.render()
|
||||||
return html_to_pdf(html, pagination=self.has_multiple_pages)
|
return html_to_pdf(html, pagination=self.has_multiple_pages)
|
||||||
|
|
||||||
def updated(self):
|
def updated(self):
|
||||||
self.updated_on = timezone.now()
|
self.updated_on = timezone.now()
|
||||||
self.save(update_fields=('updated_on',))
|
self.save(update_fields=('updated_on',))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.type:
|
if not self.type:
|
||||||
self.type = self.get_type()
|
self.type = self.get_type()
|
||||||
if not self.number:
|
if not self.number:
|
||||||
self.number = self.get_number()
|
self.number = self.get_number()
|
||||||
super(Bill, self).save(*args, **kwargs)
|
super(Bill, self).save(*args, **kwargs)
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def compute_subtotals(self):
|
def compute_subtotals(self):
|
||||||
subtotals = {}
|
subtotals = {}
|
||||||
|
@ -354,21 +354,21 @@ class Bill(models.Model):
|
||||||
for tax, subtotal in subtotals.items():
|
for tax, subtotal in subtotals.items():
|
||||||
result[tax] = [subtotal, round(tax/100*subtotal, 2)]
|
result[tax] = [subtotal, round(tax/100*subtotal, 2)]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def compute_base(self):
|
def compute_base(self):
|
||||||
bases = self.lines.annotate(
|
bases = self.lines.annotate(
|
||||||
bases=F('subtotal') + Sum(Coalesce('sublines__total', 0))
|
bases=F('subtotal') + Sum(Coalesce('sublines__total', 0))
|
||||||
)
|
)
|
||||||
return round(bases.aggregate(Sum('bases'))['bases__sum'] or 0, 2)
|
return round(bases.aggregate(Sum('bases'))['bases__sum'] or 0, 2)
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def compute_tax(self):
|
def compute_tax(self):
|
||||||
taxes = self.lines.annotate(
|
taxes = self.lines.annotate(
|
||||||
taxes=(F('subtotal') + Coalesce(Sum('sublines__total'), 0)) * (F('tax')/100)
|
taxes=(F('subtotal') + Coalesce(Sum('sublines__total'), 0)) * (F('tax')/100)
|
||||||
)
|
)
|
||||||
return round(taxes.aggregate(Sum('taxes'))['taxes__sum'] or 0, 2)
|
return round(taxes.aggregate(Sum('taxes'))['taxes__sum'] or 0, 2)
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def compute_total(self):
|
def compute_total(self):
|
||||||
if 'lines' in getattr(self, '_prefetched_objects_cache', ()):
|
if 'lines' in getattr(self, '_prefetched_objects_cache', ()):
|
||||||
|
@ -416,7 +416,7 @@ class ProForma(Bill):
|
||||||
|
|
||||||
class BillLine(models.Model):
|
class BillLine(models.Model):
|
||||||
""" Base model for bill item representation """
|
""" Base model for bill item representation """
|
||||||
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
|
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines', on_delete=models.CASCADE)
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
||||||
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
||||||
|
@ -434,24 +434,24 @@ class BillLine(models.Model):
|
||||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||||
# Amendment
|
# Amendment
|
||||||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||||
related_name='amendment_lines', null=True, blank=True)
|
related_name='amendment_lines', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'id'
|
get_latest_by = 'id'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "#%i" % self.pk if self.pk else self.description
|
return "#%i" % self.pk if self.pk else self.description
|
||||||
|
|
||||||
def get_verbose_quantity(self):
|
def get_verbose_quantity(self):
|
||||||
return self.verbose_quantity or self.quantity
|
return self.verbose_quantity or self.quantity
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if not self.verbose_quantity:
|
if not self.verbose_quantity:
|
||||||
quantity = str(self.quantity)
|
quantity = str(self.quantity)
|
||||||
# Strip trailing zeros
|
# Strip trailing zeros
|
||||||
if quantity.endswith('0'):
|
if quantity.endswith('0'):
|
||||||
self.verbose_quantity = quantity.strip('0').strip('.')
|
self.verbose_quantity = quantity.strip('0').strip('.')
|
||||||
|
|
||||||
def get_verbose_period(self):
|
def get_verbose_period(self):
|
||||||
from django.template.defaultfilters import date
|
from django.template.defaultfilters import date
|
||||||
date_format = "N 'y"
|
date_format = "N 'y"
|
||||||
|
@ -467,7 +467,7 @@ class BillLine(models.Model):
|
||||||
if ini == end:
|
if ini == end:
|
||||||
return ini
|
return ini
|
||||||
return "{ini} / {end}".format(ini=ini, end=end)
|
return "{ini} / {end}".format(ini=ini, end=end)
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def compute_total(self):
|
def compute_total(self):
|
||||||
total = self.subtotal or 0
|
total = self.subtotal or 0
|
||||||
|
@ -478,7 +478,7 @@ class BillLine(models.Model):
|
||||||
else:
|
else:
|
||||||
total += self.sublines.aggregate(sub_total=Sum('total'))['sub_total'] or 0
|
total += self.sublines.aggregate(sub_total=Sum('total'))['sub_total'] or 0
|
||||||
return round(total, 2)
|
return round(total, 2)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return change_url(self)
|
return change_url(self)
|
||||||
|
|
||||||
|
@ -493,12 +493,12 @@ class BillSubline(models.Model):
|
||||||
(COMPENSATION, _("Compensation")),
|
(COMPENSATION, _("Compensation")),
|
||||||
(OTHER, _("Other")),
|
(OTHER, _("Other")),
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: order info for undoing
|
# TODO: order info for undoing
|
||||||
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
|
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines', on_delete=models.CASCADE)
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
total = models.DecimalField(max_digits=12, decimal_places=2)
|
total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s %i" % (self.description, self.total)
|
return "%s %i" % (self.description, self.total)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from orchestra.admin.actions import SendEmail
|
||||||
from orchestra.admin.utils import insertattr, change_url
|
from orchestra.admin.utils import insertattr, change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin
|
from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from .filters import EmailUsageListFilter
|
from .filters import EmailUsageListFilter
|
||||||
from .models import Contact
|
from .models import Contact
|
||||||
|
@ -61,18 +61,18 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
actions = (SendEmail(), list_accounts)
|
actions = (SendEmail(), list_accounts)
|
||||||
|
|
||||||
def dispaly_name(self, contact):
|
def dispaly_name(self, contact):
|
||||||
return str(contact)
|
return str(contact)
|
||||||
dispaly_name.short_description = _("Name")
|
dispaly_name.short_description = _("Name")
|
||||||
dispaly_name.admin_order_field = 'short_name'
|
dispaly_name.admin_order_field = 'short_name'
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(130)
|
||||||
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,14 +86,14 @@ class ContactInline(admin.StackedInline):
|
||||||
fields = (
|
fields = (
|
||||||
('short_name', 'full_name'), 'email', 'email_usage', ('phone', 'phone2'),
|
('short_name', 'full_name'), 'email', 'email_usage', ('phone', 'phone2'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_extra(self, request, obj=None, **kwargs):
|
def get_extra(self, request, obj=None, **kwargs):
|
||||||
return 0 if obj and obj.contacts.exists() else 1
|
return 0 if obj and obj.contacts.exists() else 1
|
||||||
|
|
||||||
def get_view_on_site_url(self, obj=None):
|
def get_view_on_site_url(self, obj=None):
|
||||||
if obj:
|
if obj:
|
||||||
return change_url(obj)
|
return change_url(obj)
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'short_name':
|
if db_field.name == 'short_name':
|
||||||
|
@ -101,7 +101,7 @@ class ContactInline(admin.StackedInline):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||||
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -29,11 +29,11 @@ class Contact(models.Model):
|
||||||
('ADDS', _("Announcements")),
|
('ADDS', _("Announcements")),
|
||||||
('EMERGENCY', _("Emergency contact")),
|
('EMERGENCY', _("Emergency contact")),
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ContactQuerySet.as_manager()
|
objects = ContactQuerySet.as_manager()
|
||||||
|
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='contacts', null=True)
|
related_name='contacts', null=True, on_delete=models.SET_NULL)
|
||||||
short_name = models.CharField(_("short name"), max_length=128)
|
short_name = models.CharField(_("short name"), max_length=128)
|
||||||
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
|
@ -54,10 +54,10 @@ class Contact(models.Model):
|
||||||
country = models.CharField(_("country"), max_length=20, blank=True,
|
country = models.CharField(_("country"), max_length=20, blank=True,
|
||||||
choices=settings.CONTACTS_COUNTRIES,
|
choices=settings.CONTACTS_COUNTRIES,
|
||||||
default=settings.CONTACTS_DEFAULT_COUNTRY)
|
default=settings.CONTACTS_DEFAULT_COUNTRY)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.full_name or self.short_name
|
return self.full_name or self.short_name
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.short_name = self.short_name.strip()
|
self.short_name = self.short_name.strip()
|
||||||
self.full_name = self.full_name.strip()
|
self.full_name = self.full_name.strip()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
|
@ -49,17 +51,17 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
filter_by_account_fields = ('users',)
|
filter_by_account_fields = ('users',)
|
||||||
list_prefetch_related = ('users',)
|
list_prefetch_related = ('users',)
|
||||||
actions = (list_accounts, save_selected)
|
actions = (list_accounts, save_selected)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_users(self, db):
|
def display_users(self, db):
|
||||||
links = []
|
links = []
|
||||||
for user in db.users.all():
|
for user in db.users.all():
|
||||||
link = '<a href="%s">%s</a>' % (change_url(user), user.username)
|
link = format_html('<a href="{}">{}</a>', change_url(user), user.username)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
display_users.short_description = _("Users")
|
display_users.short_description = _("Users")
|
||||||
display_users.allow_tags = True
|
|
||||||
display_users.admin_order_field = 'users__username'
|
display_users.admin_order_field = 'users__username'
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(DatabaseAdmin, self).save_model(request, obj, form, change)
|
super(DatabaseAdmin, self).save_model(request, obj, form, change)
|
||||||
if not change:
|
if not change:
|
||||||
|
@ -98,24 +100,24 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
|
||||||
filter_by_account_fields = ('databases',)
|
filter_by_account_fields = ('databases',)
|
||||||
list_prefetch_related = ('databases',)
|
list_prefetch_related = ('databases',)
|
||||||
actions = (list_accounts, save_selected)
|
actions = (list_accounts, save_selected)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_databases(self, user):
|
def display_databases(self, user):
|
||||||
links = []
|
links = []
|
||||||
for db in user.databases.all():
|
for db in user.databases.all():
|
||||||
link = '<a href="%s">%s</a>' % (change_url(db), db.name)
|
link = format_html('<a href="{}">{}</a>', change_url(db), db.name)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
display_databases.short_description = _("Databases")
|
display_databases.short_description = _("Databases")
|
||||||
display_databases.allow_tags = True
|
|
||||||
display_databases.admin_order_field = 'databases__name'
|
display_databases.admin_order_field = 'databases__name'
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
useradmin = UserAdmin(DatabaseUser, self.admin_site)
|
useradmin = UserAdmin(DatabaseUser, self.admin_site)
|
||||||
return [
|
return [
|
||||||
url(r'^(\d+)/password/$',
|
url(r'^(\d+)/password/$',
|
||||||
self.admin_site.admin_view(useradmin.user_change_password))
|
self.admin_site.admin_view(useradmin.user_change_password))
|
||||||
] + super(DatabaseUserAdmin, self).get_urls()
|
] + super(DatabaseUserAdmin, self).get_urls()
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
""" set password """
|
""" set password """
|
||||||
if not change:
|
if not change:
|
||||||
|
|
|
@ -17,11 +17,11 @@ class DatabaseUserCreationForm(forms.ModelForm):
|
||||||
password2 = forms.CharField(label=_("Password confirmation"), required=False,
|
password2 = forms.CharField(label=_("Password confirmation"), required=False,
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput,
|
||||||
help_text=_("Enter the same password as above, for verification."))
|
help_text=_("Enter the same password as above, for verification."))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DatabaseUser
|
model = DatabaseUser
|
||||||
fields = ('username', 'account', 'type')
|
fields = ('username', 'account', 'type')
|
||||||
|
|
||||||
def clean_password2(self):
|
def clean_password2(self):
|
||||||
password1 = self.cleaned_data.get("password1")
|
password1 = self.cleaned_data.get("password1")
|
||||||
password2 = self.cleaned_data.get("password2")
|
password2 = self.cleaned_data.get("password2")
|
||||||
|
@ -40,11 +40,11 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
||||||
'invalid': _("This value may contain 16 characters or fewer, only letters, numbers and "
|
'invalid': _("This value may contain 16 characters or fewer, only letters, numbers and "
|
||||||
"@/./+/-/_ characters.")})
|
"@/./+/-/_ characters.")})
|
||||||
user = forms.ModelChoiceField(required=False, queryset=DatabaseUser.objects)
|
user = forms.ModelChoiceField(required=False, queryset=DatabaseUser.objects)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Database
|
model = Database
|
||||||
fields = ('username', 'account', 'type')
|
fields = ('username', 'account', 'type')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DatabaseCreationForm, self).__init__(*args, **kwargs)
|
super(DatabaseCreationForm, self).__init__(*args, **kwargs)
|
||||||
account_id = self.initial.get('account', self.initial_account)
|
account_id = self.initial.get('account', self.initial_account)
|
||||||
|
@ -53,13 +53,13 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
||||||
choices = [ (u.pk, "%s (%s)" % (u, u.get_type_display())) for u in qs ]
|
choices = [ (u.pk, "%s (%s)" % (u, u.get_type_display())) for u in qs ]
|
||||||
self.fields['user'].queryset = qs
|
self.fields['user'].queryset = qs
|
||||||
self.fields['user'].choices = [(None, '--------'),] + choices
|
self.fields['user'].choices = [(None, '--------'),] + choices
|
||||||
|
|
||||||
def clean_username(self):
|
def clean_username(self):
|
||||||
username = self.cleaned_data.get('username')
|
username = self.cleaned_data.get('username')
|
||||||
if DatabaseUser.objects.filter(username=username).exists():
|
if DatabaseUser.objects.filter(username=username).exists():
|
||||||
raise ValidationError("Provided username already exists.")
|
raise ValidationError("Provided username already exists.")
|
||||||
return username
|
return username
|
||||||
|
|
||||||
def clean_password2(self):
|
def clean_password2(self):
|
||||||
username = self.cleaned_data.get('username')
|
username = self.cleaned_data.get('username')
|
||||||
password1 = self.cleaned_data.get('password1')
|
password1 = self.cleaned_data.get('password1')
|
||||||
|
@ -70,14 +70,14 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
||||||
msg = _("The two password fields didn't match.")
|
msg = _("The two password fields didn't match.")
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return password2
|
return password2
|
||||||
|
|
||||||
def clean_user(self):
|
def clean_user(self):
|
||||||
user = self.cleaned_data.get('user')
|
user = self.cleaned_data.get('user')
|
||||||
if user and user.type != self.cleaned_data.get('type'):
|
if user and user.type != self.cleaned_data.get('type'):
|
||||||
msg = _("Database type and user type doesn't match")
|
msg = _("Database type and user type doesn't match")
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(DatabaseCreationForm, self).clean()
|
cleaned_data = super(DatabaseCreationForm, self).clean()
|
||||||
if 'user' in cleaned_data and 'username' in cleaned_data:
|
if 'user' in cleaned_data and 'username' in cleaned_data:
|
||||||
|
@ -91,7 +91,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
||||||
|
|
||||||
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
||||||
class ReadOnlyPasswordHashWidget(forms.Widget):
|
class ReadOnlyPasswordHashWidget(forms.Widget):
|
||||||
def render(self, name, value, attrs):
|
def render(self, name, value, attrs, renderer=None):
|
||||||
original = ReadOnlyPasswordHashField.widget().render(name, value, attrs)
|
original = ReadOnlyPasswordHashField.widget().render(name, value, attrs)
|
||||||
if 'Invalid' not in original:
|
if 'Invalid' not in original:
|
||||||
return original
|
return original
|
||||||
|
@ -114,10 +114,10 @@ class DatabaseUserChangeForm(forms.ModelForm):
|
||||||
"this user's password, but you can change the password "
|
"this user's password, but you can change the password "
|
||||||
"using <a href='../password/'>this form</a>. "
|
"using <a href='../password/'>this form</a>. "
|
||||||
"<a onclick='return showAddAnotherPopup(this);' href='../hash/'>Show hash</a>."))
|
"<a onclick='return showAddAnotherPopup(this);' href='../hash/'>Show hash</a>."))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DatabaseUser
|
model = DatabaseUser
|
||||||
fields = ('username', 'password', 'type', 'account')
|
fields = ('username', 'password', 'type', 'account')
|
||||||
|
|
||||||
def clean_password(self):
|
def clean_password(self):
|
||||||
return self.initial["password"]
|
return self.initial["password"]
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||||
('name', models.CharField(verbose_name='name', max_length=64, validators=[orchestra.core.validators.validate_name])),
|
('name', models.CharField(verbose_name='name', max_length=64, validators=[orchestra.core.validators.validate_name])),
|
||||||
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
||||||
('account', models.ForeignKey(related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -29,7 +30,7 @@ class Migration(migrations.Migration):
|
||||||
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
||||||
('password', models.CharField(verbose_name='password', max_length=256)),
|
('password', models.CharField(verbose_name='password', max_length=256)),
|
||||||
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
||||||
('account', models.ForeignKey(related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'DB users',
|
'verbose_name_plural': 'DB users',
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('databases', '0001_initial'), ('databases', '0002_auto_20170528_2005'), ('databases', '0003_database_comments'), ('databases', '0004_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Database',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
|
||||||
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DatabaseUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('username', models.CharField(max_length=16, validators=[orchestra.core.validators.validate_name], verbose_name='username')),
|
||||||
|
('password', models.CharField(max_length=256, verbose_name='password')),
|
||||||
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'DB users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='database',
|
||||||
|
name='users',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='databases', to='databases.DatabaseUser', verbose_name='users'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='databaseuser',
|
||||||
|
unique_together=set([('username', 'type')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='database',
|
||||||
|
unique_together=set([('name', 'type')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='database',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('databases', '0003_database_comments'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -12,7 +12,7 @@ class Database(models.Model):
|
||||||
""" Represents a basic database for a web application """
|
""" Represents a basic database for a web application """
|
||||||
MYSQL = 'mysql'
|
MYSQL = 'mysql'
|
||||||
POSTGRESQL = 'postgresql'
|
POSTGRESQL = 'postgresql'
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=64, # MySQL limit
|
name = models.CharField(_("name"), max_length=64, # MySQL limit
|
||||||
validators=[validators.validate_name])
|
validators=[validators.validate_name])
|
||||||
users = models.ManyToManyField('databases.DatabaseUser', blank=True,
|
users = models.ManyToManyField('databases.DatabaseUser', blank=True,
|
||||||
|
@ -20,16 +20,16 @@ class Database(models.Model):
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
default=settings.DATABASES_DEFAULT_TYPE)
|
default=settings.DATABASES_DEFAULT_TYPE)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='databases')
|
verbose_name=_("Account"), related_name='databases')
|
||||||
comments = models.TextField(default="", blank=True)
|
comments = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('name', 'type')
|
unique_together = ('name', 'type')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s" % self.name
|
return "%s" % self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def owner(self):
|
def owner(self):
|
||||||
""" database owner is the first user related to it """
|
""" database owner is the first user related to it """
|
||||||
|
@ -39,7 +39,7 @@ class Database(models.Model):
|
||||||
if user is not None:
|
if user is not None:
|
||||||
return user.databaseuser
|
return user.databaseuser
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.account.is_active
|
return self.account.is_active
|
||||||
|
@ -53,26 +53,26 @@ Database.users.through._meta.unique_together = (
|
||||||
class DatabaseUser(models.Model):
|
class DatabaseUser(models.Model):
|
||||||
MYSQL = Database.MYSQL
|
MYSQL = Database.MYSQL
|
||||||
POSTGRESQL = Database.POSTGRESQL
|
POSTGRESQL = Database.POSTGRESQL
|
||||||
|
|
||||||
username = models.CharField(_("username"), max_length=16, # MySQL usernames 16 char long
|
username = models.CharField(_("username"), max_length=16, # MySQL usernames 16 char long
|
||||||
validators=[validators.validate_name])
|
validators=[validators.validate_name])
|
||||||
password = models.CharField(_("password"), max_length=256)
|
password = models.CharField(_("password"), max_length=256)
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
default=settings.DATABASES_DEFAULT_TYPE)
|
default=settings.DATABASES_DEFAULT_TYPE)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='databaseusers')
|
verbose_name=_("Account"), related_name='databaseusers')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("DB users")
|
verbose_name_plural = _("DB users")
|
||||||
unique_together = ('username', 'type')
|
unique_together = ('username', 'type')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
def get_username(self):
|
def get_username(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
if self.type == self.MYSQL:
|
if self.type == self.MYSQL:
|
||||||
# MySQL stores sha1(sha1(password).binary).hex
|
# MySQL stores sha1(sha1(password).binary).hex
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
import MySQLdb
|
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import MySQLdb
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
|
||||||
|
|
||||||
from orchestra.admin.utils import change_url
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Route, Server
|
||||||
from orchestra.utils.sys import sshrun
|
from orchestra.utils.sys import sshrun
|
||||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error,
|
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||||
snapshot_on_error)
|
save_response_on_error, snapshot_on_error)
|
||||||
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from ... import backends, settings
|
from ... import backends, settings
|
||||||
from ...models import Database, DatabaseUser
|
from ...models import Database, DatabaseUser
|
||||||
|
|
||||||
|
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||||
|
|
||||||
|
|
||||||
class DatabaseTestMixin(object):
|
class DatabaseTestMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
|
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
|
||||||
|
@ -24,40 +26,40 @@ class DatabaseTestMixin(object):
|
||||||
'orchestra.contrib.orchestration',
|
'orchestra.contrib.orchestration',
|
||||||
'orcgestra.apps.databases',
|
'orcgestra.apps.databases',
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DatabaseTestMixin, self).setUp()
|
super(DatabaseTestMixin, self).setUp()
|
||||||
self.add_route()
|
self.add_route()
|
||||||
djsettings.DEBUG = True
|
djsettings.DEBUG = True
|
||||||
|
|
||||||
def add_route(self):
|
def add_route(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def add(self):
|
def add(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def add_group(self, username, groupname):
|
def add_group(self, username, groupname):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
self.add(dbname, username, password)
|
self.add(dbname, username, password)
|
||||||
self.validate_create_table(dbname, username, password)
|
self.validate_create_table(dbname, username, password)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -68,7 +70,7 @@ class DatabaseTestMixin(object):
|
||||||
self.delete_user(username)
|
self.delete_user(username)
|
||||||
self.validate_delete(dbname, username, password)
|
self.validate_delete(dbname, username, password)
|
||||||
self.validate_delete_user(dbname, username)
|
self.validate_delete_user(dbname, username)
|
||||||
|
|
||||||
def test_change_password(self):
|
def test_change_password(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -81,7 +83,7 @@ class DatabaseTestMixin(object):
|
||||||
self.change_password(username, new_password)
|
self.change_password(username, new_password)
|
||||||
self.validate_login_error(dbname, username, password)
|
self.validate_login_error(dbname, username, password)
|
||||||
self.validate_create_table(dbname, username, new_password)
|
self.validate_create_table(dbname, username, new_password)
|
||||||
|
|
||||||
def test_add_user(self):
|
def test_add_user(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -98,7 +100,7 @@ class DatabaseTestMixin(object):
|
||||||
self.add_user_to_db(username2, dbname)
|
self.add_user_to_db(username2, dbname)
|
||||||
self.validate_create_table(dbname, username, password)
|
self.validate_create_table(dbname, username, password)
|
||||||
self.validate_create_table(dbname, username2, password2)
|
self.validate_create_table(dbname, username2, password2)
|
||||||
|
|
||||||
def test_delete_user(self):
|
def test_delete_user(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -117,7 +119,7 @@ class DatabaseTestMixin(object):
|
||||||
self.delete_user(username2)
|
self.delete_user(username2)
|
||||||
self.validate_login_error(dbname, username2, password2)
|
self.validate_login_error(dbname, username2, password2)
|
||||||
self.validate_delete_user(username2, password2)
|
self.validate_delete_user(username2, password2)
|
||||||
|
|
||||||
def test_swap_user(self):
|
def test_swap_user(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -137,7 +139,7 @@ class DatabaseTestMixin(object):
|
||||||
|
|
||||||
class MySQLControllerMixin(object):
|
class MySQLControllerMixin(object):
|
||||||
db_type = 'mysql'
|
db_type = 'mysql'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(MySQLControllerMixin, self).setUp()
|
super(MySQLControllerMixin, self).setUp()
|
||||||
# Get local ip address used to reach self.MASTER_SERVER
|
# Get local ip address used to reach self.MASTER_SERVER
|
||||||
|
@ -145,7 +147,7 @@ class MySQLControllerMixin(object):
|
||||||
s.connect((self.MASTER_SERVER, 22))
|
s.connect((self.MASTER_SERVER, 22))
|
||||||
settings.DATABASES_DEFAULT_HOST = s.getsockname()[0]
|
settings.DATABASES_DEFAULT_HOST = s.getsockname()[0]
|
||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
def add_route(self):
|
def add_route(self):
|
||||||
server = Server.objects.create(name=self.MASTER_SERVER)
|
server = Server.objects.create(name=self.MASTER_SERVER)
|
||||||
backend = backends.MySQLController.get_name()
|
backend = backends.MySQLController.get_name()
|
||||||
|
@ -154,22 +156,22 @@ class MySQLControllerMixin(object):
|
||||||
match = "databaseuser.type == '%s'" % self.db_type
|
match = "databaseuser.type == '%s'" % self.db_type
|
||||||
backend = backends.MySQLUserController.get_name()
|
backend = backends.MySQLUserController.get_name()
|
||||||
Route.objects.create(backend=backend, match=match, host=server)
|
Route.objects.create(backend=backend, match=match, host=server)
|
||||||
|
|
||||||
def validate_create_table(self, name, username, password):
|
def validate_create_table(self, name, username, password):
|
||||||
db = MySQLdb.connect(host=self.MASTER_SERVER, port=3306, user=username, passwd=password, db=name)
|
db = MySQLdb.connect(host=self.MASTER_SERVER, port=3306, user=username, passwd=password, db=name)
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
cur.execute('CREATE TABLE table_%s ( id INT ) ;' % random_ascii(10))
|
cur.execute('CREATE TABLE table_%s ( id INT ) ;' % random_ascii(10))
|
||||||
|
|
||||||
def validate_login_error(self, dbname, username, password):
|
def validate_login_error(self, dbname, username, password):
|
||||||
self.assertRaises(MySQLdb.OperationalError,
|
self.assertRaises(MySQLdb.OperationalError,
|
||||||
self.validate_create_table, dbname, username, password
|
self.validate_create_table, dbname, username, password
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_delete(self, dbname, username, password):
|
def validate_delete(self, dbname, username, password):
|
||||||
self.validate_login_error(dbname, username, password)
|
self.validate_login_error(dbname, username, password)
|
||||||
self.assertRaises(CommandError,
|
self.assertRaises(CommandError,
|
||||||
sshrun, self.MASTER_SERVER, 'mysql %s' % dbname, display=False)
|
sshrun, self.MASTER_SERVER, 'mysql %s' % dbname, display=False)
|
||||||
|
|
||||||
def validate_delete_user(self, name, username):
|
def validate_delete_user(self, name, username):
|
||||||
context = {
|
context = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
@ -181,11 +183,12 @@ class MySQLControllerMixin(object):
|
||||||
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
|
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||||
class RESTDatabaseMixin(DatabaseTestMixin):
|
class RESTDatabaseMixin(DatabaseTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTDatabaseMixin, self).setUp()
|
super(RESTDatabaseMixin, self).setUp()
|
||||||
self.rest_login()
|
self.rest_login()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def add(self, dbname, username, password):
|
def add(self, dbname, username, password):
|
||||||
user = self.rest.databaseusers.create(username=username, password=password, type=self.db_type)
|
user = self.rest.databaseusers.create(username=username, password=password, type=self.db_type)
|
||||||
|
@ -193,31 +196,31 @@ class RESTDatabaseMixin(DatabaseTestMixin):
|
||||||
'username': user.username
|
'username': user.username
|
||||||
}]
|
}]
|
||||||
self.rest.databases.create(name=dbname, users=users, type=self.db_type)
|
self.rest.databases.create(name=dbname, users=users, type=self.db_type)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def delete(self, dbname):
|
def delete(self, dbname):
|
||||||
self.rest.databases.retrieve(name=dbname).delete()
|
self.rest.databases.retrieve(name=dbname).delete()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def change_password(self, username, password):
|
def change_password(self, username, password):
|
||||||
user = self.rest.databaseusers.retrieve(username=username).get()
|
user = self.rest.databaseusers.retrieve(username=username).get()
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def add_user(self, username, password):
|
def add_user(self, username, password):
|
||||||
self.rest.databaseusers.create(username=username, password=password, type=self.db_type)
|
self.rest.databaseusers.create(username=username, password=password, type=self.db_type)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def add_user_to_db(self, username, dbname):
|
def add_user_to_db(self, username, dbname):
|
||||||
user = self.rest.databaseusers.retrieve(username=username).get()
|
user = self.rest.databaseusers.retrieve(username=username).get()
|
||||||
db = self.rest.databases.retrieve(name=dbname).get()
|
db = self.rest.databases.retrieve(name=dbname).get()
|
||||||
db.users.append(user)
|
db.users.append(user)
|
||||||
db.save()
|
db.save()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def delete_user(self, username):
|
def delete_user(self, username):
|
||||||
self.rest.databaseusers.retrieve(username=username).delete()
|
self.rest.databaseusers.retrieve(username=username).delete()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def swap_user(self, username, username2, dbname):
|
def swap_user(self, username, username2, dbname):
|
||||||
user = self.rest.databaseusers.retrieve(username=username2).get()
|
user = self.rest.databaseusers.retrieve(username=username2).get()
|
||||||
|
@ -231,84 +234,84 @@ class AdminDatabaseMixin(DatabaseTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(AdminDatabaseMixin, self).setUp()
|
super(AdminDatabaseMixin, self).setUp()
|
||||||
self.admin_login()
|
self.admin_login()
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def add(self, dbname, username, password):
|
def add(self, dbname, username, password):
|
||||||
url = self.live_server_url + reverse('admin:databases_database_add')
|
url = self.live_server_url + reverse('admin:databases_database_add')
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
type_input = self.selenium.find_element_by_id('id_type')
|
type_input = self.selenium.find_element_by_id('id_type')
|
||||||
type_select = Select(type_input)
|
type_select = Select(type_input)
|
||||||
type_select.select_by_value(self.db_type)
|
type_select.select_by_value(self.db_type)
|
||||||
|
|
||||||
name_field = self.selenium.find_element_by_id('id_name')
|
name_field = self.selenium.find_element_by_id('id_name')
|
||||||
name_field.send_keys(dbname)
|
name_field.send_keys(dbname)
|
||||||
|
|
||||||
username_field = self.selenium.find_element_by_id('id_username')
|
username_field = self.selenium.find_element_by_id('id_username')
|
||||||
username_field.send_keys(username)
|
username_field.send_keys(username)
|
||||||
|
|
||||||
password_field = self.selenium.find_element_by_id('id_password1')
|
password_field = self.selenium.find_element_by_id('id_password1')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
password_field = self.selenium.find_element_by_id('id_password2')
|
password_field = self.selenium.find_element_by_id('id_password2')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
|
|
||||||
name_field.submit()
|
name_field.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def delete(self, dbname):
|
def delete(self, dbname):
|
||||||
db = Database.objects.get(name=dbname)
|
db = Database.objects.get(name=dbname)
|
||||||
self.admin_delete(db)
|
self.admin_delete(db)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def change_password(self, username, password):
|
def change_password(self, username, password):
|
||||||
user = DatabaseUser.objects.get(username=username)
|
user = DatabaseUser.objects.get(username=username)
|
||||||
self.admin_change_password(user, password)
|
self.admin_change_password(user, password)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def add_user(self, username, password):
|
def add_user(self, username, password):
|
||||||
url = self.live_server_url + reverse('admin:databases_databaseuser_add')
|
url = self.live_server_url + reverse('admin:databases_databaseuser_add')
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
type_input = self.selenium.find_element_by_id('id_type')
|
type_input = self.selenium.find_element_by_id('id_type')
|
||||||
type_select = Select(type_input)
|
type_select = Select(type_input)
|
||||||
type_select.select_by_value(self.db_type)
|
type_select.select_by_value(self.db_type)
|
||||||
|
|
||||||
username_field = self.selenium.find_element_by_id('id_username')
|
username_field = self.selenium.find_element_by_id('id_username')
|
||||||
username_field.send_keys(username)
|
username_field.send_keys(username)
|
||||||
|
|
||||||
password_field = self.selenium.find_element_by_id('id_password1')
|
password_field = self.selenium.find_element_by_id('id_password1')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
password_field = self.selenium.find_element_by_id('id_password2')
|
password_field = self.selenium.find_element_by_id('id_password2')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
|
|
||||||
username_field.submit()
|
username_field.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def add_user_to_db(self, username, dbname):
|
def add_user_to_db(self, username, dbname):
|
||||||
database = Database.objects.get(name=dbname, type=self.db_type)
|
database = Database.objects.get(name=dbname, type=self.db_type)
|
||||||
url = self.live_server_url + change_url(database)
|
url = self.live_server_url + change_url(database)
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
user = DatabaseUser.objects.get(username=username, type=self.db_type)
|
user = DatabaseUser.objects.get(username=username, type=self.db_type)
|
||||||
users_from = self.selenium.find_element_by_id('id_users_from')
|
users_from = self.selenium.find_element_by_id('id_users_from')
|
||||||
users_select = Select(users_from)
|
users_select = Select(users_from)
|
||||||
users_select.select_by_value(str(user.pk))
|
users_select.select_by_value(str(user.pk))
|
||||||
|
|
||||||
add_user = self.selenium.find_element_by_id('id_users_add_link')
|
add_user = self.selenium.find_element_by_id('id_users_add_link')
|
||||||
add_user.click()
|
add_user.click()
|
||||||
|
|
||||||
save = self.selenium.find_element_by_name('_save')
|
save = self.selenium.find_element_by_name('_save')
|
||||||
save.submit()
|
save.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def swap_user(self, username, username2, dbname):
|
def swap_user(self, username, username2, dbname):
|
||||||
database = Database.objects.get(name=dbname, type=self.db_type)
|
database = Database.objects.get(name=dbname, type=self.db_type)
|
||||||
url = self.live_server_url + change_url(database)
|
url = self.live_server_url + change_url(database)
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
# remove user "username"
|
# remove user "username"
|
||||||
user = DatabaseUser.objects.get(username=username, type=self.db_type)
|
user = DatabaseUser.objects.get(username=username, type=self.db_type)
|
||||||
users_to = self.selenium.find_element_by_id('id_users_to')
|
users_to = self.selenium.find_element_by_id('id_users_to')
|
||||||
|
@ -317,7 +320,7 @@ class AdminDatabaseMixin(DatabaseTestMixin):
|
||||||
remove_user = self.selenium.find_element_by_id('id_users_remove_link')
|
remove_user = self.selenium.find_element_by_id('id_users_remove_link')
|
||||||
remove_user.click()
|
remove_user.click()
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
# add user "username2"
|
# add user "username2"
|
||||||
user = DatabaseUser.objects.get(username=username2, type=self.db_type)
|
user = DatabaseUser.objects.get(username=username2, type=self.db_type)
|
||||||
users_from = self.selenium.find_element_by_id('id_users_from')
|
users_from = self.selenium.find_element_by_id('id_users_from')
|
||||||
|
@ -326,11 +329,11 @@ class AdminDatabaseMixin(DatabaseTestMixin):
|
||||||
add_user = self.selenium.find_element_by_id('id_users_add_link')
|
add_user = self.selenium.find_element_by_id('id_users_add_link')
|
||||||
add_user.click()
|
add_user.click()
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
save = self.selenium.find_element_by_name('_save')
|
save = self.selenium.find_element_by_name('_save')
|
||||||
save.submit()
|
save.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def delete_user(self, username):
|
def delete_user(self, username):
|
||||||
user = DatabaseUser.objects.get(username=username)
|
user = DatabaseUser.objects.get(username=username)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
|
@ -32,18 +34,18 @@ class DomainInline(admin.TabularInline):
|
||||||
readonly_fields = ('domain_link', 'display_records', 'account_link')
|
readonly_fields = ('domain_link', 'display_records', 'account_link')
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = _("Subdomains")
|
verbose_name_plural = _("Subdomains")
|
||||||
|
|
||||||
domain_link = admin_link('__str__')
|
domain_link = admin_link('__str__')
|
||||||
domain_link.short_description = _("Name")
|
domain_link.short_description = _("Name")
|
||||||
account_link = admin_link('account')
|
account_link = admin_link('account')
|
||||||
|
|
||||||
def display_records(self, domain):
|
def display_records(self, domain):
|
||||||
return ', '.join([record.type for record in domain.records.all()])
|
return ', '.join([record.type for record in domain.records.all()])
|
||||||
display_records.short_description = _("Declared records")
|
display_records.short_description = _("Declared records")
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
qs = super(DomainInline, self).get_queryset(request)
|
qs = super(DomainInline, self).get_queryset(request)
|
||||||
|
@ -66,23 +68,23 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
add_form = BatchDomainCreationAdminForm
|
add_form = BatchDomainCreationAdminForm
|
||||||
actions = (edit_records, set_soa, list_accounts)
|
actions = (edit_records, set_soa, list_accounts)
|
||||||
change_view_actions = (view_zone, edit_records)
|
change_view_actions = (view_zone, edit_records)
|
||||||
|
|
||||||
top_link = admin_link('top')
|
top_link = admin_link('top')
|
||||||
|
|
||||||
def structured_name(self, domain):
|
def structured_name(self, domain):
|
||||||
if domain.is_top:
|
if domain.is_top:
|
||||||
return domain.name
|
return domain.name
|
||||||
return ' '*4 + domain.name
|
return mark_safe(' '*4 + domain.name)
|
||||||
structured_name.short_description = _("name")
|
structured_name.short_description = _("name")
|
||||||
structured_name.allow_tags = True
|
|
||||||
structured_name.admin_order_field = 'structured_name'
|
structured_name.admin_order_field = 'structured_name'
|
||||||
|
|
||||||
def display_is_top(self, domain):
|
def display_is_top(self, domain):
|
||||||
return domain.is_top
|
return domain.is_top
|
||||||
display_is_top.short_description = _("Is top")
|
display_is_top.short_description = _("Is top")
|
||||||
display_is_top.boolean = True
|
display_is_top.boolean = True
|
||||||
display_is_top.admin_order_field = 'top'
|
display_is_top.admin_order_field = 'top'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_websites(self, domain):
|
def display_websites(self, domain):
|
||||||
if apps.isinstalled('orchestra.contrib.websites'):
|
if apps.isinstalled('orchestra.contrib.websites'):
|
||||||
websites = domain.websites.all()
|
websites = domain.websites.all()
|
||||||
|
@ -92,22 +94,22 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
site_link = get_on_site_link(website.get_absolute_url())
|
site_link = get_on_site_link(website.get_absolute_url())
|
||||||
admin_url = change_url(website)
|
admin_url = change_url(website)
|
||||||
title = _("Edit website")
|
title = _("Edit website")
|
||||||
link = '<a href="%s" title="%s">%s %s</a>' % (
|
link = format_html('<a href="{}" title="{}">{} {}</a>',
|
||||||
admin_url, title, website.name, site_link)
|
admin_url, title, website.name, site_link)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
add_url = reverse('admin:websites_website_add')
|
add_url = reverse('admin:websites_website_add')
|
||||||
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
||||||
image = '<img src="%s"></img>' % static('orchestra/images/add.png')
|
add_link = format_html(
|
||||||
add_link = '<a href="%s" title="%s">%s</a>' % (
|
'<a href="{}" title="{}"><img src="{}" /></a>', add_url,
|
||||||
add_url, _("Add website"), image
|
_("Add website"), static('orchestra/images/add.png'),
|
||||||
)
|
)
|
||||||
return _("No website %s") % (add_link)
|
return _("No website %s") % (add_link)
|
||||||
return '---'
|
return '---'
|
||||||
display_websites.admin_order_field = 'websites__name'
|
display_websites.admin_order_field = 'websites__name'
|
||||||
display_websites.short_description = _("Websites")
|
display_websites.short_description = _("Websites")
|
||||||
display_websites.allow_tags = True
|
|
||||||
|
@mark_safe
|
||||||
def display_addresses(self, domain):
|
def display_addresses(self, domain):
|
||||||
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
||||||
add_url = reverse('admin:mailboxes_address_add')
|
add_url = reverse('admin:mailboxes_address_add')
|
||||||
|
@ -126,10 +128,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
return '---'
|
return '---'
|
||||||
display_addresses.short_description = _("Addresses")
|
display_addresses.short_description = _("Addresses")
|
||||||
display_addresses.admin_order_field = 'addresses__count'
|
display_addresses.admin_order_field = 'addresses__count'
|
||||||
display_addresses.allow_tags = True
|
|
||||||
|
@mark_safe
|
||||||
def implicit_records(self, domain):
|
def implicit_records(self, domain):
|
||||||
defaults = []
|
|
||||||
types = set(domain.records.values_list('type', flat=True))
|
types = set(domain.records.values_list('type', flat=True))
|
||||||
ttl = settings.DOMAINS_DEFAULT_TTL
|
ttl = settings.DOMAINS_DEFAULT_TTL
|
||||||
lines = []
|
lines = []
|
||||||
|
@ -141,15 +142,14 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
value=record.value
|
value=record.value
|
||||||
)
|
)
|
||||||
if not domain.record_is_implicit(record, types):
|
if not domain.record_is_implicit(record, types):
|
||||||
line = '<strike>%s</strike>' % line
|
line = format_html('<strike>{}</strike>', line)
|
||||||
if record.type is Record.SOA:
|
if record.type is Record.SOA:
|
||||||
lines.insert(0, line)
|
lines.insert(0, line)
|
||||||
else:
|
else:
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return '<br>'.join(lines)
|
return '<br>'.join(lines)
|
||||||
implicit_records.short_description = _("Implicit records")
|
implicit_records.short_description = _("Implicit records")
|
||||||
implicit_records.allow_tags = True
|
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
""" Add SOA fields when domain is top """
|
""" Add SOA fields when domain is top """
|
||||||
fieldsets = super(DomainAdmin, self).get_fieldsets(request, obj)
|
fieldsets = super(DomainAdmin, self).get_fieldsets(request, obj)
|
||||||
|
@ -175,13 +175,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
if 'top_link' not in existing:
|
if 'top_link' not in existing:
|
||||||
fieldsets[0][1]['fields'].insert(2, 'top_link')
|
fieldsets[0][1]['fields'].insert(2, 'top_link')
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
inlines = super(DomainAdmin, self).get_inline_instances(request, obj)
|
inlines = super(DomainAdmin, self).get_inline_instances(request, obj)
|
||||||
if not obj or not obj.is_top:
|
if not obj or not obj.is_top:
|
||||||
return [inline for inline in inlines if type(inline) != DomainInline]
|
return [inline for inline in inlines if type(inline) != DomainInline]
|
||||||
return inlines
|
return inlines
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
qs = super(DomainAdmin, self).get_queryset(request)
|
qs = super(DomainAdmin, self).get_queryset(request)
|
||||||
|
@ -196,7 +196,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
||||||
qs = qs.annotate(models.Count('addresses'))
|
qs = qs.annotate(models.Count('addresses'))
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
""" batch domain creation support """
|
""" batch domain creation support """
|
||||||
super(DomainAdmin, self).save_model(request, obj, form, change)
|
super(DomainAdmin, self).save_model(request, obj, form, change)
|
||||||
|
@ -205,7 +205,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
for name in form.extra_names:
|
for name in form.extra_names:
|
||||||
domain = Domain.objects.create(name=name, account_id=obj.account_id)
|
domain = Domain.objects.create(name=name, account_id=obj.account_id)
|
||||||
self.extra_domains.append(domain)
|
self.extra_domains.append(domain)
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
def save_related(self, request, form, formsets, change):
|
||||||
""" batch domain creation support """
|
""" batch domain creation support """
|
||||||
super(DomainAdmin, self).save_related(request, form, formsets, change)
|
super(DomainAdmin, self).save_related(request, form, formsets, change)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from orchestra.api import router
|
from orchestra.api import router
|
||||||
|
@ -14,18 +14,18 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
||||||
serializer_class = DomainSerializer
|
serializer_class = DomainSerializer
|
||||||
filter_fields = ('name',)
|
filter_fields = ('name',)
|
||||||
queryset = Domain.objects.all()
|
queryset = Domain.objects.all()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super(DomainViewSet, self).get_queryset()
|
qs = super(DomainViewSet, self).get_queryset()
|
||||||
return qs.prefetch_related('records')
|
return qs.prefetch_related('records')
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def view_zone(self, request, pk=None):
|
def view_zone(self, request, pk=None):
|
||||||
domain = self.get_object()
|
domain = self.get_object()
|
||||||
return Response({
|
return Response({
|
||||||
'zone': domain.render_zone()
|
'zone': domain.render_zone()
|
||||||
})
|
})
|
||||||
|
|
||||||
def options(self, request):
|
def options(self, request):
|
||||||
metadata = super(DomainViewSet, self).options(request)
|
metadata = super(DomainViewSet, self).options(request)
|
||||||
names = ['DOMAINS_DEFAULT_A', 'DOMAINS_DEFAULT_MX', 'DOMAINS_DEFAULT_NS']
|
names = ['DOMAINS_DEFAULT_A', 'DOMAINS_DEFAULT_MX', 'DOMAINS_DEFAULT_NS']
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.domains.utils
|
import orchestra.contrib.domains.utils
|
||||||
import orchestra.contrib.domains.validators
|
import orchestra.contrib.domains.validators
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,8 +21,8 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
('name', models.CharField(unique=True, max_length=256, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name', help_text='Domain or subdomain name.')),
|
('name', models.CharField(unique=True, max_length=256, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name', help_text='Domain or subdomain name.')),
|
||||||
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, verbose_name='serial', help_text='Serial number')),
|
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, verbose_name='serial', help_text='Serial number')),
|
||||||
('account', models.ForeignKey(related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
|
||||||
('top', models.ForeignKey(null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
('top', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -31,7 +32,7 @@ class Migration(migrations.Migration):
|
||||||
('ttl', models.CharField(help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL', blank=True)),
|
('ttl', models.CharField(help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL', blank=True)),
|
||||||
('type', models.CharField(max_length=32, verbose_name='type', choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SOA', 'SOA')])),
|
('type', models.CharField(max_length=32, verbose_name='type', choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SOA', 'SOA')])),
|
||||||
('value', models.CharField(max_length=256, verbose_name='value')),
|
('value', models.CharField(max_length=256, verbose_name='value')),
|
||||||
('domain', models.ForeignKey(related_name='records', to='domains.Domain', verbose_name='domain')),
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.contrib.domains.utils
|
||||||
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('domains', '0001_initial'), ('domains', '0002_auto_20150715_1017'), ('domains', '0003_auto_20150720_1121'), ('domains', '0004_auto_20150720_1121'), ('domains', '0005_auto_20160219_1034'), ('domains', '0006_auto_20170528_2011'), ('domains', '0007_auto_20190805_1134'), ('domains', '0008_domain_dns2136_address_match_list'), ('domains', '0009_auto_20200204_1217'), ('domains', '0010_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Domain',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name')),
|
||||||
|
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')),
|
||||||
|
('account', models.ForeignKey(blank=True, help_text='Automatically selected for subdomains.', on_delete=django.db.models.deletion.CASCADE, related_name='domains', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('top', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Record',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ttl', models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL')),
|
||||||
|
('type', models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF')], max_length=32, verbose_name='type')),
|
||||||
|
('value', models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value')),
|
||||||
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='serial',
|
||||||
|
field=models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, editable=False, help_text='A revision number that changes whenever this domain is updated.', verbose_name='serial'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='expire',
|
||||||
|
field=models.CharField(blank=True, help_text='The time that a secondary server will keep trying to complete a zone transfer. If this time expires prior to a successful zone transfer, the secondary server will expire its zone file. This means the secondary will stop answering queries. The default value is <tt>4w</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='expire'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='min_ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='refresh',
|
||||||
|
field=models.CharField(blank=True, help_text="The time a secondary DNS server waits before querying the primary DNS server's SOA record to check for changes. When the refresh time expires, the secondary DNS server requests a copy of the current SOA record from the primary. The primary DNS server complies with this request. The secondary DNS server compares the serial number of the primary DNS server's current SOA record and the serial number in it's own SOA record. If they are different, the secondary DNS server will request a zone transfer from the primary DNS server. The default value is <tt>1d</tt>.", max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='refresh'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='retry',
|
||||||
|
field=models.CharField(blank=True, help_text='The time a secondary server waits before retrying a failed zone transfer. Normally, the retry time is less than the refresh time. The default value is <tt>2h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='retry'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='top',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain', verbose_name='top domain'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='dns2136_address_match_list',
|
||||||
|
field=models.CharField(blank=True, default='key pangea.key;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.domains.validators
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='domain',
|
model_name='domain',
|
||||||
name='top',
|
name='top',
|
||||||
field=models.ForeignKey(editable=False, verbose_name='top domain', related_name='subdomain_set', to='domains.Domain', null=True),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, editable=False, verbose_name='top domain', related_name='subdomain_set', to='domains.Domain', null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='record',
|
model_name='record',
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0009_auto_20200204_1217'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='min_ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='record',
|
||||||
|
name='ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -31,9 +31,9 @@ class Domain(models.Model):
|
||||||
validators.validate_allowed_domain
|
validators.validate_allowed_domain
|
||||||
])
|
])
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
|
||||||
related_name='domains', help_text=_("Automatically selected for subdomains."))
|
related_name='domains', on_delete=models.CASCADE, help_text=_("Automatically selected for subdomains."))
|
||||||
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
|
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
|
||||||
editable=False, verbose_name=_("top domain"))
|
editable=False, verbose_name=_("top domain"), on_delete=models.CASCADE)
|
||||||
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
|
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
|
||||||
help_text=_("A revision number that changes whenever this domain is updated."))
|
help_text=_("A revision number that changes whenever this domain is updated."))
|
||||||
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
|
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
|
||||||
|
@ -69,16 +69,16 @@ class Domain(models.Model):
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="A bind-9 'address_match_list' that will be granted permission to perform "
|
help_text="A bind-9 'address_match_list' that will be granted permission to perform "
|
||||||
"dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.")
|
"dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.")
|
||||||
|
|
||||||
objects = DomainQuerySet.as_manager()
|
objects = DomainQuerySet.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def origin(self):
|
def origin(self):
|
||||||
return self.top or self
|
return self.top or self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_top(self):
|
def is_top(self):
|
||||||
# don't cache, don't replace by top_id
|
# don't cache, don't replace by top_id
|
||||||
|
@ -86,14 +86,14 @@ class Domain(models.Model):
|
||||||
return not bool(self.top)
|
return not bool(self.top)
|
||||||
except Domain.DoesNotExist:
|
except Domain.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subdomains(self):
|
def subdomains(self):
|
||||||
return Domain.objects.filter(name__regex='\.%s$' % self.name)
|
return Domain.objects.filter(name__regex='\.%s$' % self.name)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.name = self.name.lower()
|
self.name = self.name.lower()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
""" create top relation """
|
""" create top relation """
|
||||||
update = False
|
update = False
|
||||||
|
@ -110,7 +110,7 @@ class Domain(models.Model):
|
||||||
# queryset.update() is not used because we want to trigger backend to delete ex-topdomains
|
# queryset.update() is not used because we want to trigger backend to delete ex-topdomains
|
||||||
domain.top = self
|
domain.top = self
|
||||||
domain.save(update_fields=('top',))
|
domain.save(update_fields=('top',))
|
||||||
|
|
||||||
def get_description(self):
|
def get_description(self):
|
||||||
if self.is_top:
|
if self.is_top:
|
||||||
num = self.subdomains.count()
|
num = self.subdomains.count()
|
||||||
|
@ -119,21 +119,21 @@ class Domain(models.Model):
|
||||||
_("top domain with %d subdomains") % num,
|
_("top domain with %d subdomains") % num,
|
||||||
num)
|
num)
|
||||||
return _("subdomain")
|
return _("subdomain")
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return 'http://%s' % self.name
|
return 'http://%s' % self.name
|
||||||
|
|
||||||
def get_declared_records(self):
|
def get_declared_records(self):
|
||||||
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
||||||
return self.records.all()
|
return self.records.all()
|
||||||
|
|
||||||
def get_subdomains(self):
|
def get_subdomains(self):
|
||||||
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
||||||
return self.origin.subdomain_set.all().prefetch_related('records')
|
return self.origin.subdomain_set.all().prefetch_related('records')
|
||||||
|
|
||||||
def get_parent(self, top=False):
|
def get_parent(self, top=False):
|
||||||
return type(self).objects.get_parent(self.name, top=top)
|
return type(self).objects.get_parent(self.name, top=top)
|
||||||
|
|
||||||
def render_zone(self):
|
def render_zone(self):
|
||||||
origin = self.origin
|
origin = self.origin
|
||||||
zone = origin.render_records()
|
zone = origin.render_records()
|
||||||
|
@ -147,7 +147,7 @@ class Domain(models.Model):
|
||||||
for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True):
|
for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True):
|
||||||
zone += subdomain.render_records()
|
zone += subdomain.render_records()
|
||||||
return zone.strip()
|
return zone.strip()
|
||||||
|
|
||||||
def refresh_serial(self):
|
def refresh_serial(self):
|
||||||
""" Increases the domain serial number by one """
|
""" Increases the domain serial number by one """
|
||||||
serial = utils.generate_zone_serial()
|
serial = utils.generate_zone_serial()
|
||||||
|
@ -159,7 +159,7 @@ class Domain(models.Model):
|
||||||
serial = int(serial)
|
serial = int(serial)
|
||||||
self.serial = serial
|
self.serial = serial
|
||||||
self.save(update_fields=('serial',))
|
self.save(update_fields=('serial',))
|
||||||
|
|
||||||
def get_default_soa(self):
|
def get_default_soa(self):
|
||||||
return ' '.join([
|
return ' '.join([
|
||||||
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
|
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
|
||||||
|
@ -170,7 +170,7 @@ class Domain(models.Model):
|
||||||
self.expire or settings.DOMAINS_DEFAULT_EXPIRE,
|
self.expire or settings.DOMAINS_DEFAULT_EXPIRE,
|
||||||
self.min_ttl or settings.DOMAINS_DEFAULT_MIN_TTL,
|
self.min_ttl or settings.DOMAINS_DEFAULT_MIN_TTL,
|
||||||
])
|
])
|
||||||
|
|
||||||
def get_default_records(self):
|
def get_default_records(self):
|
||||||
defaults = []
|
defaults = []
|
||||||
if self.is_top:
|
if self.is_top:
|
||||||
|
@ -202,7 +202,7 @@ class Domain(models.Model):
|
||||||
value=default_aaaa
|
value=default_aaaa
|
||||||
))
|
))
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
def record_is_implicit(self, record, types):
|
def record_is_implicit(self, record, types):
|
||||||
if record.type not in types:
|
if record.type not in types:
|
||||||
if record.type is Record.NS:
|
if record.type is Record.NS:
|
||||||
|
@ -221,7 +221,7 @@ class Domain(models.Model):
|
||||||
elif not has_a and not has_aaaa:
|
elif not has_a and not has_aaaa:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_records(self):
|
def get_records(self):
|
||||||
types = set()
|
types = set()
|
||||||
records = utils.RecordStorage()
|
records = utils.RecordStorage()
|
||||||
|
@ -249,7 +249,7 @@ class Domain(models.Model):
|
||||||
else:
|
else:
|
||||||
records.append(record)
|
records.append(record)
|
||||||
return records
|
return records
|
||||||
|
|
||||||
def render_records(self):
|
def render_records(self):
|
||||||
result = ''
|
result = ''
|
||||||
for record in self.get_records():
|
for record in self.get_records():
|
||||||
|
@ -273,7 +273,7 @@ class Domain(models.Model):
|
||||||
value=record.value
|
value=record.value
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def has_default_mx(self):
|
def has_default_mx(self):
|
||||||
records = self.get_records()
|
records = self.get_records()
|
||||||
for record in records.by_type('MX'):
|
for record in records.by_type('MX'):
|
||||||
|
@ -294,7 +294,7 @@ class Record(models.Model):
|
||||||
TXT = 'TXT'
|
TXT = 'TXT'
|
||||||
SPF = 'SPF'
|
SPF = 'SPF'
|
||||||
SOA = 'SOA'
|
SOA = 'SOA'
|
||||||
|
|
||||||
TYPE_CHOICES = (
|
TYPE_CHOICES = (
|
||||||
(MX, "MX"),
|
(MX, "MX"),
|
||||||
(NS, "NS"),
|
(NS, "NS"),
|
||||||
|
@ -305,7 +305,7 @@ class Record(models.Model):
|
||||||
(TXT, "TXT"),
|
(TXT, "TXT"),
|
||||||
(SPF, "SPF"),
|
(SPF, "SPF"),
|
||||||
)
|
)
|
||||||
|
|
||||||
VALIDATORS = {
|
VALIDATORS = {
|
||||||
MX: (validators.validate_mx_record,),
|
MX: (validators.validate_mx_record,),
|
||||||
NS: (validators.validate_zone_label,),
|
NS: (validators.validate_zone_label,),
|
||||||
|
@ -317,8 +317,8 @@ class Record(models.Model):
|
||||||
SRV: (validators.validate_srv_record,),
|
SRV: (validators.validate_srv_record,),
|
||||||
SOA: (validators.validate_soa_record,),
|
SOA: (validators.validate_soa_record,),
|
||||||
}
|
}
|
||||||
|
|
||||||
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
|
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE)
|
||||||
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
||||||
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
||||||
validators=[validators.validate_zone_interval])
|
validators=[validators.validate_zone_interval])
|
||||||
|
@ -326,10 +326,10 @@ class Record(models.Model):
|
||||||
# max_length bumped from 256 to 1024 (arbitrary) on August 2019.
|
# max_length bumped from 256 to 1024 (arbitrary) on August 2019.
|
||||||
value = models.CharField(_("value"), max_length=1024,
|
value = models.CharField(_("value"), max_length=1024,
|
||||||
help_text=_("MX, NS and CNAME records sould end with a dot."))
|
help_text=_("MX, NS and CNAME records sould end with a dot."))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
|
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
""" validates record value based on its type """
|
""" validates record value based on its type """
|
||||||
# validate value
|
# validate value
|
||||||
|
@ -343,6 +343,6 @@ class Record(models.Model):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'value': error,
|
'value': error,
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_ttl(self):
|
def get_ttl(self):
|
||||||
return self.ttl or settings.DOMAINS_DEFAULT_TTL
|
return self.ttl or settings.DOMAINS_DEFAULT_TTL
|
||||||
|
|
|
@ -4,7 +4,7 @@ import socket
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Server, Route
|
||||||
|
@ -23,7 +23,7 @@ class DomainTestMixin(object):
|
||||||
SLAVE_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
SLAVE_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||||
MASTER_SERVER_ADDR = socket.gethostbyname(MASTER_SERVER)
|
MASTER_SERVER_ADDR = socket.gethostbyname(MASTER_SERVER)
|
||||||
SLAVE_SERVER_ADDR = socket.gethostbyname(SLAVE_SERVER)
|
SLAVE_SERVER_ADDR = socket.gethostbyname(SLAVE_SERVER)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
djsettings.DEBUG = True
|
djsettings.DEBUG = True
|
||||||
super(DomainTestMixin, self).setUp()
|
super(DomainTestMixin, self).setUp()
|
||||||
|
@ -53,19 +53,19 @@ class DomainTestMixin(object):
|
||||||
(Record.CNAME, 'external.server.org.'),
|
(Record.CNAME, 'external.server.org.'),
|
||||||
)
|
)
|
||||||
self.django_domain_name = 'django%s.lan' % random_ascii(10)
|
self.django_domain_name = 'django%s.lan' % random_ascii(10)
|
||||||
|
|
||||||
def add_route(self):
|
def add_route(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def add(self, domain_name, records):
|
def add(self, domain_name, records):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def delete(self, domain_name, records):
|
def delete(self, domain_name, records):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self, domain_name, records):
|
def update(self, domain_name, records):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def validate_add(self, server_addr, domain_name):
|
def validate_add(self, server_addr, domain_name):
|
||||||
context = {
|
context = {
|
||||||
'domain_name': domain_name,
|
'domain_name': domain_name,
|
||||||
|
@ -81,7 +81,7 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('%s.' % settings.DOMAINS_DEFAULT_NAME_SERVER, soa[4])
|
self.assertEqual('%s.' % settings.DOMAINS_DEFAULT_NAME_SERVER, soa[4])
|
||||||
hostmaster = utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER)
|
hostmaster = utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER)
|
||||||
self.assertEqual(hostmaster, soa[5])
|
self.assertEqual(hostmaster, soa[5])
|
||||||
|
|
||||||
dig_ns = 'dig @%(server_addr)s %(domain_name)s NS|grep "\sNS\s"'
|
dig_ns = 'dig @%(server_addr)s %(domain_name)s NS|grep "\sNS\s"'
|
||||||
name_servers = run(dig_ns % context).stdout
|
name_servers = run(dig_ns % context).stdout
|
||||||
# testdomain.org. 3600 IN NS ns1.orchestra.lan.
|
# testdomain.org. 3600 IN NS ns1.orchestra.lan.
|
||||||
|
@ -95,7 +95,7 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('IN', ns[2])
|
self.assertEqual('IN', ns[2])
|
||||||
self.assertEqual('NS', ns[3])
|
self.assertEqual('NS', ns[3])
|
||||||
self.assertIn(ns[4], ns_records)
|
self.assertIn(ns[4], ns_records)
|
||||||
|
|
||||||
dig_mx = 'dig @%(server_addr)s %(domain_name)s MX|grep "\sMX\s"'
|
dig_mx = 'dig @%(server_addr)s %(domain_name)s MX|grep "\sMX\s"'
|
||||||
mail_servers = run(dig_mx % context).stdout
|
mail_servers = run(dig_mx % context).stdout
|
||||||
for mx in mail_servers.splitlines():
|
for mx in mail_servers.splitlines():
|
||||||
|
@ -107,7 +107,7 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('MX', mx[3])
|
self.assertEqual('MX', mx[3])
|
||||||
self.assertIn(mx[4], ['10', '20'])
|
self.assertIn(mx[4], ['10', '20'])
|
||||||
self.assertIn(mx[5], ['mail2.orchestra.lan.', 'mail.orchestra.lan.'])
|
self.assertIn(mx[5], ['mail2.orchestra.lan.', 'mail.orchestra.lan.'])
|
||||||
|
|
||||||
def validate_delete(self, server_addr, domain_name):
|
def validate_delete(self, server_addr, domain_name):
|
||||||
context = {
|
context = {
|
||||||
'domain_name': domain_name,
|
'domain_name': domain_name,
|
||||||
|
@ -122,7 +122,7 @@ class DomainTestMixin(object):
|
||||||
self.assertNotEqual('%s.' % settings.DOMAINS_DEFAULT_NAME_SERVER, soa[4])
|
self.assertNotEqual('%s.' % settings.DOMAINS_DEFAULT_NAME_SERVER, soa[4])
|
||||||
hostmaster = utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER)
|
hostmaster = utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER)
|
||||||
self.assertNotEqual(hostmaster, soa[5])
|
self.assertNotEqual(hostmaster, soa[5])
|
||||||
|
|
||||||
def validate_update(self, server_addr, domain_name):
|
def validate_update(self, server_addr, domain_name):
|
||||||
context = {
|
context = {
|
||||||
'domain_name': domain_name,
|
'domain_name': domain_name,
|
||||||
|
@ -138,7 +138,7 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('%s.' % settings.DOMAINS_DEFAULT_NAME_SERVER, soa[4])
|
self.assertEqual('%s.' % settings.DOMAINS_DEFAULT_NAME_SERVER, soa[4])
|
||||||
hostmaster = utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER)
|
hostmaster = utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER)
|
||||||
self.assertEqual(hostmaster, soa[5])
|
self.assertEqual(hostmaster, soa[5])
|
||||||
|
|
||||||
dig_ns = 'dig @%(server_addr)s %(domain_name)s NS |grep "\sNS\s"'
|
dig_ns = 'dig @%(server_addr)s %(domain_name)s NS |grep "\sNS\s"'
|
||||||
name_servers = run(dig_ns % context).stdout
|
name_servers = run(dig_ns % context).stdout
|
||||||
ns_records = ['ns1.%s.' % self.domain_name, 'ns2.%s.' % self.domain_name]
|
ns_records = ['ns1.%s.' % self.domain_name, 'ns2.%s.' % self.domain_name]
|
||||||
|
@ -151,7 +151,7 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('IN', ns[2])
|
self.assertEqual('IN', ns[2])
|
||||||
self.assertEqual('NS', ns[3])
|
self.assertEqual('NS', ns[3])
|
||||||
self.assertIn(ns[4], ns_records)
|
self.assertIn(ns[4], ns_records)
|
||||||
|
|
||||||
dig_mx = 'dig @%(server_addr)s %(domain_name)s MX | grep "\sMX\s"'
|
dig_mx = 'dig @%(server_addr)s %(domain_name)s MX | grep "\sMX\s"'
|
||||||
mx = run(dig_mx % context).stdout.split()
|
mx = run(dig_mx % context).stdout.split()
|
||||||
# testdomain.org. 3600 IN MX 10 orchestra.lan.
|
# testdomain.org. 3600 IN MX 10 orchestra.lan.
|
||||||
|
@ -161,7 +161,7 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('MX', mx[3])
|
self.assertEqual('MX', mx[3])
|
||||||
self.assertIn(mx[4], ['30', '40'])
|
self.assertIn(mx[4], ['30', '40'])
|
||||||
self.assertIn(mx[5], ['mail3.orchestra.lan.', 'mail4.orchestra.lan.'])
|
self.assertIn(mx[5], ['mail3.orchestra.lan.', 'mail4.orchestra.lan.'])
|
||||||
|
|
||||||
def validate_www_update(self, server_addr, domain_name):
|
def validate_www_update(self, server_addr, domain_name):
|
||||||
context = {
|
context = {
|
||||||
'domain_name': domain_name,
|
'domain_name': domain_name,
|
||||||
|
@ -175,7 +175,7 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('IN', cname[2])
|
self.assertEqual('IN', cname[2])
|
||||||
self.assertEqual('CNAME', cname[3])
|
self.assertEqual('CNAME', cname[3])
|
||||||
self.assertEqual('external.server.org.', cname[4])
|
self.assertEqual('external.server.org.', cname[4])
|
||||||
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -184,7 +184,7 @@ class DomainTestMixin(object):
|
||||||
self.validate_add(self.MASTER_SERVER_ADDR, self.domain_name)
|
self.validate_add(self.MASTER_SERVER_ADDR, self.domain_name)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.validate_add(self.SLAVE_SERVER_ADDR, self.domain_name)
|
self.validate_add(self.SLAVE_SERVER_ADDR, self.domain_name)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -193,7 +193,7 @@ class DomainTestMixin(object):
|
||||||
for name in [self.domain_name, self.ns1_name, self.ns2_name]:
|
for name in [self.domain_name, self.ns1_name, self.ns2_name]:
|
||||||
self.validate_delete(self.MASTER_SERVER_ADDR, name)
|
self.validate_delete(self.MASTER_SERVER_ADDR, name)
|
||||||
self.validate_delete(self.SLAVE_SERVER_ADDR, name)
|
self.validate_delete(self.SLAVE_SERVER_ADDR, name)
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -209,7 +209,7 @@ class DomainTestMixin(object):
|
||||||
self.validate_www_update(self.MASTER_SERVER_ADDR, self.domain_name)
|
self.validate_www_update(self.MASTER_SERVER_ADDR, self.domain_name)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
self.validate_www_update(self.SLAVE_SERVER_ADDR, self.domain_name)
|
self.validate_www_update(self.SLAVE_SERVER_ADDR, self.domain_name)
|
||||||
|
|
||||||
def test_add_add_delete_delete(self):
|
def test_add_add_delete_delete(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -221,7 +221,7 @@ class DomainTestMixin(object):
|
||||||
self.delete(self.django_domain_name)
|
self.delete(self.django_domain_name)
|
||||||
self.validate_delete(self.MASTER_SERVER_ADDR, self.django_domain_name)
|
self.validate_delete(self.MASTER_SERVER_ADDR, self.django_domain_name)
|
||||||
self.validate_delete(self.SLAVE_SERVER_ADDR, self.django_domain_name)
|
self.validate_delete(self.SLAVE_SERVER_ADDR, self.django_domain_name)
|
||||||
|
|
||||||
def test_bad_creation(self):
|
def test_bad_creation(self):
|
||||||
self.assertRaises((self.rest.ResponseStatusError, AssertionError),
|
self.assertRaises((self.rest.ResponseStatusError, AssertionError),
|
||||||
self.add, self.domain_name, self.domain_records)
|
self.add, self.domain_name, self.domain_records)
|
||||||
|
@ -232,7 +232,7 @@ class AdminDomainMixin(DomainTestMixin):
|
||||||
super(AdminDomainMixin, self).setUp()
|
super(AdminDomainMixin, self).setUp()
|
||||||
self.add_route()
|
self.add_route()
|
||||||
self.admin_login()
|
self.admin_login()
|
||||||
|
|
||||||
def _add_records(self, records):
|
def _add_records(self, records):
|
||||||
self.selenium.find_element_by_link_text('Add another Record').click()
|
self.selenium.find_element_by_link_text('Add another Record').click()
|
||||||
for i, record in zip(range(0, len(records)), records):
|
for i, record in zip(range(0, len(records)), records):
|
||||||
|
@ -244,29 +244,29 @@ class AdminDomainMixin(DomainTestMixin):
|
||||||
value_input.clear()
|
value_input.clear()
|
||||||
value_input.send_keys(value)
|
value_input.send_keys(value)
|
||||||
return value_input
|
return value_input
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def add(self, domain_name, records):
|
def add(self, domain_name, records):
|
||||||
add = reverse('admin:domains_domain_add')
|
add = reverse('admin:domains_domain_add')
|
||||||
url = self.live_server_url + add
|
url = self.live_server_url + add
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
name = self.selenium.find_element_by_id('id_name')
|
name = self.selenium.find_element_by_id('id_name')
|
||||||
name.send_keys(domain_name)
|
name.send_keys(domain_name)
|
||||||
|
|
||||||
account_input = self.selenium.find_element_by_id('id_account')
|
account_input = self.selenium.find_element_by_id('id_account')
|
||||||
account_select = Select(account_input)
|
account_select = Select(account_input)
|
||||||
account_select.select_by_value(str(self.account.pk))
|
account_select.select_by_value(str(self.account.pk))
|
||||||
|
|
||||||
value_input = self._add_records(records)
|
value_input = self._add_records(records)
|
||||||
value_input.submit()
|
value_input.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def delete(self, domain_name):
|
def delete(self, domain_name):
|
||||||
domain = Domain.objects.get(name=domain_name)
|
domain = Domain.objects.get(name=domain_name)
|
||||||
self.admin_delete(domain)
|
self.admin_delete(domain)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def update(self, domain_name, records):
|
def update(self, domain_name, records):
|
||||||
domain = Domain.objects.get(name=domain_name)
|
domain = Domain.objects.get(name=domain_name)
|
||||||
|
@ -283,18 +283,18 @@ class RESTDomainMixin(DomainTestMixin):
|
||||||
super(RESTDomainMixin, self).setUp()
|
super(RESTDomainMixin, self).setUp()
|
||||||
self.rest_login()
|
self.rest_login()
|
||||||
self.add_route()
|
self.add_route()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def add(self, domain_name, records):
|
def add(self, domain_name, records):
|
||||||
records = [ dict(type=type, value=value) for type,value in records ]
|
records = [ dict(type=type, value=value) for type,value in records ]
|
||||||
self.rest.domains.create(name=domain_name, records=records)
|
self.rest.domains.create(name=domain_name, records=records)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def delete(self, domain_name):
|
def delete(self, domain_name):
|
||||||
domain = Domain.objects.get(name=domain_name)
|
domain = Domain.objects.get(name=domain_name)
|
||||||
domain = self.rest.domains.retrieve(id=domain.pk)
|
domain = self.rest.domains.retrieve(id=domain.pk)
|
||||||
domain.delete()
|
domain.delete()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def update(self, domain_name, records):
|
def update(self, domain_name, records):
|
||||||
records = [ dict(type=type, value=value) for type,value in records ]
|
records = [ dict(type=type, value=value) for type,value in records ]
|
||||||
|
@ -307,7 +307,7 @@ class Bind9BackendMixin(object):
|
||||||
DEPENDENCIES = (
|
DEPENDENCIES = (
|
||||||
'orchestra.contrib.orchestration',
|
'orchestra.contrib.orchestration',
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_route(self):
|
def add_route(self):
|
||||||
master = Server.objects.create(name=self.MASTER_SERVER, address=self.MASTER_SERVER_ADDR)
|
master = Server.objects.create(name=self.MASTER_SERVER, address=self.MASTER_SERVER_ADDR)
|
||||||
backend = backends.Bind9MasterDomainController.get_name()
|
backend = backends.Bind9MasterDomainController.get_name()
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
|
||||||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.contrib.admin.utils import unquote
|
|
||||||
from django.contrib.admin.templatetags.admin_static import static
|
from django.contrib.admin.templatetags.admin_static import static
|
||||||
|
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||||
|
from django.contrib.admin.utils import unquote
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.utils import admin_link, admin_date
|
from orchestra.admin.utils import admin_date, admin_link
|
||||||
|
|
||||||
|
|
||||||
class LogEntryAdmin(admin.ModelAdmin):
|
class LogEntryAdmin(admin.ModelAdmin):
|
||||||
|
@ -30,15 +32,16 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
list_select_related = ('user', 'content_type')
|
list_select_related = ('user', 'content_type')
|
||||||
list_display_links = None
|
list_display_links = None
|
||||||
|
|
||||||
user_link = admin_link('user')
|
user_link = admin_link('user')
|
||||||
display_action_time = admin_date('action_time', short_description=_("Time"))
|
display_action_time = admin_date('action_time', short_description=_("Time"))
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_message(self, log):
|
def display_message(self, log):
|
||||||
edit = '<a href="%(url)s"><img src="%(img)s"></img></a>' % {
|
edit = format_html('<a href="{url}"><img src="{img}"></img></a>', **{
|
||||||
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
|
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
|
||||||
'img': static('admin/img/icon-changelink.svg'),
|
'img': static('admin/img/icon-changelink.svg'),
|
||||||
}
|
})
|
||||||
if log.is_addition():
|
if log.is_addition():
|
||||||
return _('Added "%(link)s". %(edit)s') % {
|
return _('Added "%(link)s". %(edit)s') % {
|
||||||
'link': self.content_object_link(log),
|
'link': self.content_object_link(log),
|
||||||
|
@ -57,8 +60,7 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
}
|
}
|
||||||
display_message.short_description = _("Message")
|
display_message.short_description = _("Message")
|
||||||
display_message.admin_order_field = 'action_flag'
|
display_message.admin_order_field = 'action_flag'
|
||||||
display_message.allow_tags = True
|
|
||||||
|
|
||||||
def display_action(self, log):
|
def display_action(self, log):
|
||||||
if log.is_addition():
|
if log.is_addition():
|
||||||
return _("Added")
|
return _("Added")
|
||||||
|
@ -67,7 +69,7 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
return _("Deleted")
|
return _("Deleted")
|
||||||
display_action.short_description = _("Action")
|
display_action.short_description = _("Action")
|
||||||
display_action.admin_order_field = 'action_flag'
|
display_action.admin_order_field = 'action_flag'
|
||||||
|
|
||||||
def content_object_link(self, log):
|
def content_object_link(self, log):
|
||||||
ct = log.content_type
|
ct = log.content_type
|
||||||
view = 'admin:%s_%s_change' % (ct.app_label, ct.model)
|
view = 'admin:%s_%s_change' % (ct.app_label, ct.model)
|
||||||
|
@ -75,11 +77,10 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
url = reverse(view, args=(log.object_id,))
|
url = reverse(view, args=(log.object_id,))
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return log.object_repr
|
return log.object_repr
|
||||||
return '<a href="%s">%s</a>' % (url, log.object_repr)
|
return format_html('<a href="{}">{}</a>', url, log.object_repr)
|
||||||
content_object_link.short_description = _("Content object")
|
content_object_link.short_description = _("Content object")
|
||||||
content_object_link.admin_order_field = 'object_repr'
|
content_object_link.admin_order_field = 'object_repr'
|
||||||
content_object_link.allow_tags = True
|
|
||||||
|
|
||||||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
""" Add rel_opts and object to context """
|
""" Add rel_opts and object to context """
|
||||||
if not add and 'edit' in request.GET.urlencode():
|
if not add and 'edit' in request.GET.urlencode():
|
||||||
|
@ -89,14 +90,14 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
})
|
})
|
||||||
return super(LogEntryAdmin, self).render_change_form(
|
return super(LogEntryAdmin, self).render_change_form(
|
||||||
request, context, add, change, form_url, obj)
|
request, context, add, change, form_url, obj)
|
||||||
|
|
||||||
def response_change(self, request, obj):
|
def response_change(self, request, obj):
|
||||||
""" save and continue preserve edit query string """
|
""" save and continue preserve edit query string """
|
||||||
response = super(LogEntryAdmin, self).response_change(request, obj)
|
response = super(LogEntryAdmin, self).response_change(request, obj)
|
||||||
if 'edit' in request.GET.urlencode() and 'edit' not in response.url:
|
if 'edit' in request.GET.urlencode() and 'edit' not in response.url:
|
||||||
return HttpResponseRedirect(response.url + '?edit=True')
|
return HttpResponseRedirect(response.url + '?edit=True')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def response_post_save_change(self, request, obj):
|
def response_post_save_change(self, request, obj):
|
||||||
""" save redirect to object history """
|
""" save redirect to object history """
|
||||||
if 'edit' in request.GET.urlencode():
|
if 'edit' in request.GET.urlencode():
|
||||||
|
@ -109,19 +110,19 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
}, post_url)
|
}, post_url)
|
||||||
return HttpResponseRedirect(post_url)
|
return HttpResponseRedirect(post_url)
|
||||||
return super(LogEntryAdmin, self).response_post_save_change(request, obj)
|
return super(LogEntryAdmin, self).response_post_save_change(request, obj)
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_delete_permission(self, *args, **kwargs):
|
def has_delete_permission(self, *args, **kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def log_addition(self, *args, **kwargs):
|
def log_addition(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def log_change(self, *args, **kwargs):
|
def log_change(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def log_deletion(self, *args, **kwargs):
|
def log_deletion(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import format_html, strip_tags
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
|
@ -21,14 +22,14 @@ from .helpers import get_ticket_changes, markdown_formated_changes, filter_actio
|
||||||
from .models import Ticket, Queue, Message
|
from .models import Ticket, Queue, Message
|
||||||
|
|
||||||
|
|
||||||
PRIORITY_COLORS = {
|
PRIORITY_COLORS = {
|
||||||
Ticket.HIGH: 'red',
|
Ticket.HIGH: 'red',
|
||||||
Ticket.MEDIUM: 'darkorange',
|
Ticket.MEDIUM: 'darkorange',
|
||||||
Ticket.LOW: 'green',
|
Ticket.LOW: 'green',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
STATE_COLORS = {
|
STATE_COLORS = {
|
||||||
Ticket.NEW: 'grey',
|
Ticket.NEW: 'grey',
|
||||||
Ticket.IN_PROGRESS: 'darkorange',
|
Ticket.IN_PROGRESS: 'darkorange',
|
||||||
Ticket.FEEDBACK: 'purple',
|
Ticket.FEEDBACK: 'purple',
|
||||||
|
@ -44,12 +45,13 @@ class MessageReadOnlyInline(admin.TabularInline):
|
||||||
can_delete = False
|
can_delete = False
|
||||||
fields = ('content_html',)
|
fields = ('content_html',)
|
||||||
readonly_fields = ('content_html',)
|
readonly_fields = ('content_html',)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('orchestra/css/hide-inline-id.css',)
|
'all': ('orchestra/css/hide-inline-id.css',)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def content_html(self, msg):
|
def content_html(self, msg):
|
||||||
context = {
|
context = {
|
||||||
'number': msg.number,
|
'number': msg.number,
|
||||||
|
@ -58,16 +60,17 @@ class MessageReadOnlyInline(admin.TabularInline):
|
||||||
}
|
}
|
||||||
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
||||||
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
||||||
|
|
||||||
content = markdown(msg.content)
|
content = markdown(msg.content)
|
||||||
content = content.replace('>\n', '>')
|
content = content.replace('>\n', '>')
|
||||||
content = '<div style="padding-left:20px;">%s</div>' % content
|
content = '<div style="padding-left:20px;">%s</div>' % content
|
||||||
|
|
||||||
return header + content
|
return header + content
|
||||||
content_html.short_description = _("Content")
|
content_html.short_description = _("Content")
|
||||||
content_html.allow_tags = True
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -79,12 +82,12 @@ class MessageInline(admin.TabularInline):
|
||||||
form = MessageInlineForm
|
form = MessageInlineForm
|
||||||
can_delete = False
|
can_delete = False
|
||||||
fields = ('content',)
|
fields = ('content',)
|
||||||
|
|
||||||
def get_formset(self, request, obj=None, **kwargs):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
""" hook request.user on the inline form """
|
""" hook request.user on the inline form """
|
||||||
self.form.user = request.user
|
self.form.user = request.user
|
||||||
return super(MessageInline, self).get_formset(request, obj, **kwargs)
|
return super(MessageInline, self).get_formset(request, obj, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Don't show any message """
|
""" Don't show any message """
|
||||||
qs = super(MessageInline, self).get_queryset(request)
|
qs = super(MessageInline, self).get_queryset(request)
|
||||||
|
@ -103,18 +106,18 @@ class TicketInline(admin.TabularInline):
|
||||||
model = Ticket
|
model = Ticket
|
||||||
extra = 0
|
extra = 0
|
||||||
max_num = 0
|
max_num = 0
|
||||||
|
|
||||||
creator_link = admin_link('creator')
|
creator_link = admin_link('creator')
|
||||||
owner_link = admin_link('owner')
|
owner_link = admin_link('owner')
|
||||||
created = admin_link('created_at')
|
created = admin_link('created_at')
|
||||||
updated = admin_link('updated_at')
|
updated = admin_link('updated_at')
|
||||||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def ticket_id(self, instance):
|
def ticket_id(self, instance):
|
||||||
return '<b>%s</b>' % admin_link()(instance)
|
return '<b>%s</b>' % admin_link()(instance)
|
||||||
ticket_id.short_description = '#'
|
ticket_id.short_description = '#'
|
||||||
ticket_id.allow_tags = True
|
|
||||||
|
|
||||||
|
|
||||||
class TicketAdmin(ExtendedModelAdmin):
|
class TicketAdmin(ExtendedModelAdmin):
|
||||||
|
@ -135,7 +138,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
'owner__username'
|
'owner__username'
|
||||||
)
|
)
|
||||||
actions = (
|
actions = (
|
||||||
mark_as_unread, mark_as_read, 'delete_selected', reject_tickets,
|
mark_as_unread, mark_as_read, reject_tickets,
|
||||||
resolve_tickets, close_tickets, take_tickets
|
resolve_tickets, close_tickets, take_tickets
|
||||||
)
|
)
|
||||||
sudo_actions = ('delete_selected',)
|
sudo_actions = ('delete_selected',)
|
||||||
|
@ -176,7 +179,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
list_select_related = ('queue', 'owner', 'creator')
|
list_select_related = ('queue', 'owner', 'creator')
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('issues/css/ticket-admin.css',)
|
'all': ('issues/css/ticket-admin.css',)
|
||||||
|
@ -184,14 +187,15 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
js = (
|
js = (
|
||||||
'issues/js/ticket-admin.js',
|
'issues/js/ticket-admin.js',
|
||||||
)
|
)
|
||||||
|
|
||||||
display_creator = admin_link('creator')
|
display_creator = admin_link('creator')
|
||||||
display_queue = admin_link('queue')
|
display_queue = admin_link('queue')
|
||||||
display_owner = admin_link('owner')
|
display_owner = admin_link('owner')
|
||||||
updated = admin_date('updated_at')
|
updated = admin_date('updated_at')
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_summary(self, ticket):
|
def display_summary(self, ticket):
|
||||||
context = {
|
context = {
|
||||||
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
||||||
|
@ -207,50 +211,47 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
||||||
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
||||||
display_summary.short_description = 'Summary'
|
display_summary.short_description = 'Summary'
|
||||||
display_summary.allow_tags = True
|
|
||||||
|
|
||||||
def unbold_id(self, ticket):
|
def unbold_id(self, ticket):
|
||||||
""" Unbold id if ticket is read """
|
""" Unbold id if ticket is read """
|
||||||
if ticket.is_read_by(self.user):
|
if ticket.is_read_by(self.user):
|
||||||
return '<span style="font-weight:normal;font-size:11px;">%s</span>' % ticket.pk
|
return format_html('<span style="font-weight:normal;font-size:11px;">{}</span>', ticket.pk)
|
||||||
return ticket.pk
|
return ticket.pk
|
||||||
unbold_id.allow_tags = True
|
|
||||||
unbold_id.short_description = "#"
|
unbold_id.short_description = "#"
|
||||||
unbold_id.admin_order_field = 'id'
|
unbold_id.admin_order_field = 'id'
|
||||||
|
|
||||||
def bold_subject(self, ticket):
|
def bold_subject(self, ticket):
|
||||||
""" Bold subject when tickets are unread for request.user """
|
""" Bold subject when tickets are unread for request.user """
|
||||||
if ticket.is_read_by(self.user):
|
if ticket.is_read_by(self.user):
|
||||||
return ticket.subject
|
return ticket.subject
|
||||||
return "<strong class='unread'>%s</strong>" % ticket.subject
|
return format_html("<strong class='unread'>{}</strong>", ticket.subject)
|
||||||
bold_subject.allow_tags = True
|
|
||||||
bold_subject.short_description = _("Subject")
|
bold_subject.short_description = _("Subject")
|
||||||
bold_subject.admin_order_field = 'subject'
|
bold_subject.admin_order_field = 'subject'
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'subject':
|
if db_field.name == 'subject':
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'120'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'120'})
|
||||||
return super(TicketAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(TicketAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def save_model(self, request, obj, *args, **kwargs):
|
def save_model(self, request, obj, *args, **kwargs):
|
||||||
""" Define creator for new tickets """
|
""" Define creator for new tickets """
|
||||||
if not obj.pk:
|
if not obj.pk:
|
||||||
obj.creator = request.user
|
obj.creator = request.user
|
||||||
super(TicketAdmin, self).save_model(request, obj, *args, **kwargs)
|
super(TicketAdmin, self).save_model(request, obj, *args, **kwargs)
|
||||||
obj.mark_as_read_by(request.user)
|
obj.mark_as_read_by(request.user)
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
""" add markdown preview url """
|
""" add markdown preview url """
|
||||||
return [
|
return [
|
||||||
url(r'^preview/$',
|
url(r'^preview/$',
|
||||||
wrap_admin_view(self, self.message_preview_view))
|
wrap_admin_view(self, self.message_preview_view))
|
||||||
] + super(TicketAdmin, self).get_urls()
|
] + super(TicketAdmin, self).get_urls()
|
||||||
|
|
||||||
def add_view(self, request, form_url='', extra_context=None):
|
def add_view(self, request, form_url='', extra_context=None):
|
||||||
""" Do not sow message inlines """
|
""" Do not sow message inlines """
|
||||||
return super(TicketAdmin, self).add_view(request, form_url, extra_context)
|
return super(TicketAdmin, self).add_view(request, form_url, extra_context)
|
||||||
|
|
||||||
def change_view(self, request, object_id, form_url='', extra_context=None):
|
def change_view(self, request, object_id, form_url='', extra_context=None):
|
||||||
""" Change view actions based on ticket state """
|
""" Change view actions based on ticket state """
|
||||||
ticket = get_object_or_404(Ticket, pk=object_id)
|
ticket = get_object_or_404(Ticket, pk=object_id)
|
||||||
|
@ -269,12 +270,12 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(TicketAdmin, self).change_view(request, object_id, form_url=form_url,
|
return super(TicketAdmin, self).change_view(request, object_id, form_url=form_url,
|
||||||
extra_context=context)
|
extra_context=context)
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
# Hook user for bold_subject
|
# Hook user for bold_subject
|
||||||
self.user = request.user
|
self.user = request.user
|
||||||
return super(TicketAdmin,self).changelist_view(request, extra_context=extra_context)
|
return super(TicketAdmin,self).changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
def message_preview_view(self, request):
|
def message_preview_view(self, request):
|
||||||
""" markdown preview render via ajax """
|
""" markdown preview render via ajax """
|
||||||
data = request.POST.get("data")
|
data = request.POST.get("data")
|
||||||
|
@ -287,21 +288,20 @@ class QueueAdmin(admin.ModelAdmin):
|
||||||
actions = (set_default_queue,)
|
actions = (set_default_queue,)
|
||||||
inlines = (TicketInline,)
|
inlines = (TicketInline,)
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('orchestra/css/hide-inline-id.css',)
|
'all': ('orchestra/css/hide-inline-id.css',)
|
||||||
}
|
}
|
||||||
|
|
||||||
def num_tickets(self, queue):
|
def num_tickets(self, queue):
|
||||||
num = queue.tickets__count
|
num = queue.tickets__count
|
||||||
url = reverse('admin:issues_ticket_changelist')
|
url = reverse('admin:issues_ticket_changelist')
|
||||||
url += '?queue=%i' % queue.pk
|
url += '?queue=%i' % queue.pk
|
||||||
return '<a href="%s">%d</a>' % (url, num)
|
return format_html('<a href="{}">{}</a>', url, num)
|
||||||
num_tickets.short_description = _("Tickets")
|
num_tickets.short_description = _("Tickets")
|
||||||
num_tickets.admin_order_field = 'tickets__count'
|
num_tickets.admin_order_field = 'tickets__count'
|
||||||
num_tickets.allow_tags = True
|
|
||||||
|
|
||||||
def get_list_display(self, request):
|
def get_list_display(self, request):
|
||||||
""" show notifications """
|
""" show notifications """
|
||||||
list_display = list(self.list_display)
|
list_display = list(self.list_display)
|
||||||
|
@ -312,7 +312,7 @@ class QueueAdmin(admin.ModelAdmin):
|
||||||
display_notify.boolean = True
|
display_notify.boolean = True
|
||||||
list_display.append(display_notify)
|
list_display.append(display_notify)
|
||||||
return list_display
|
return list_display
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(QueueAdmin, self).get_queryset(request)
|
qs = super(QueueAdmin, self).get_queryset(request)
|
||||||
qs = qs.annotate(models.Count('tickets'))
|
qs = qs.annotate(models.Count('tickets'))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import viewsets, mixins
|
from rest_framework import viewsets, mixins
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from orchestra.api import router, LogApiMixin
|
from orchestra.api import router, LogApiMixin
|
||||||
|
@ -12,19 +12,19 @@ from .serializers import TicketSerializer, QueueSerializer
|
||||||
class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
|
class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Ticket.objects.all()
|
queryset = Ticket.objects.all()
|
||||||
serializer_class = TicketSerializer
|
serializer_class = TicketSerializer
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def mark_as_read(self, request, pk=None):
|
def mark_as_read(self, request, pk=None):
|
||||||
ticket = self.get_object()
|
ticket = self.get_object()
|
||||||
ticket.mark_as_read_by(request.user)
|
ticket.mark_as_read_by(request.user)
|
||||||
return Response({'status': 'Ticket marked as read'})
|
return Response({'status': 'Ticket marked as read'})
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def mark_as_unread(self, request, pk=None):
|
def mark_as_unread(self, request, pk=None):
|
||||||
ticket = self.get_object()
|
ticket = self.get_object()
|
||||||
ticket.mark_as_unread_by(request.user)
|
ticket.mark_as_unread_by(request.user)
|
||||||
return Response({'status': 'Ticket marked as unread'})
|
return Response({'status': 'Ticket marked as unread'})
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super(TicketViewSet, self).get_queryset()
|
qs = super(TicketViewSet, self).get_queryset()
|
||||||
qs = qs.select_related('creator', 'queue')
|
qs = qs.select_related('creator', 'queue')
|
||||||
|
|
|
@ -13,7 +13,7 @@ from .models import Queue, Ticket
|
||||||
|
|
||||||
class MarkDownWidget(forms.Textarea):
|
class MarkDownWidget(forms.Textarea):
|
||||||
""" MarkDown textarea widget with syntax preview """
|
""" MarkDown textarea widget with syntax preview """
|
||||||
|
|
||||||
markdown_url = static('issues/markdown_syntax.html')
|
markdown_url = static('issues/markdown_syntax.html')
|
||||||
markdown_help_text = (
|
markdown_help_text = (
|
||||||
'<a href="%s" onclick=\'window.open("%s", "", "resizable=yes, '
|
'<a href="%s" onclick=\'window.open("%s", "", "resizable=yes, '
|
||||||
|
@ -21,8 +21,8 @@ class MarkDownWidget(forms.Textarea):
|
||||||
'return false;\'>markdown format</a>' % (markdown_url, markdown_url)
|
'return false;\'>markdown format</a>' % (markdown_url, markdown_url)
|
||||||
)
|
)
|
||||||
markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text
|
markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text
|
||||||
|
|
||||||
def render(self, name, value, attrs):
|
def render(self, name, value, attrs, renderer=None):
|
||||||
widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name
|
widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name
|
||||||
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
||||||
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
||||||
|
@ -35,18 +35,18 @@ class MessageInlineForm(forms.ModelForm):
|
||||||
""" Add message form """
|
""" Add message form """
|
||||||
created_on = forms.CharField(label="Created On", required=False)
|
created_on = forms.CharField(label="Created On", required=False)
|
||||||
content = forms.CharField(widget=MarkDownWidget(), required=False)
|
content = forms.CharField(widget=MarkDownWidget(), required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('author', 'author_name', 'created_on', 'content')
|
fields = ('author', 'author_name', 'created_on', 'content')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['created_on'].widget = SpanWidget(display='')
|
self.fields['created_on'].widget = SpanWidget(display='')
|
||||||
|
|
||||||
def clean_content(self):
|
def clean_content(self):
|
||||||
""" clean HTML tags """
|
""" clean HTML tags """
|
||||||
return strip_tags(self.cleaned_data['content'])
|
return strip_tags(self.cleaned_data['content'])
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.instance.pk is None:
|
if self.instance.pk is None:
|
||||||
self.instance.author = self.user
|
self.instance.author = self.user
|
||||||
|
@ -58,7 +58,7 @@ class UsersIterator(forms.models.ModelChoiceIterator):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.ticket = kwargs.pop('ticket', False)
|
self.ticket = kwargs.pop('ticket', False)
|
||||||
super(forms.models.ModelChoiceIterator, self).__init__(*args, **kwargs)
|
super(forms.models.ModelChoiceIterator, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield ('', '---------')
|
yield ('', '---------')
|
||||||
users = get_user_model().objects.exclude(is_active=False).order_by('name')
|
users = get_user_model().objects.exclude(is_active=False).order_by('name')
|
||||||
|
@ -73,14 +73,14 @@ class UsersIterator(forms.models.ModelChoiceIterator):
|
||||||
class TicketForm(forms.ModelForm):
|
class TicketForm(forms.ModelForm):
|
||||||
display_description = forms.CharField(label=_("Description"), required=False)
|
display_description = forms.CharField(label=_("Description"), required=False)
|
||||||
description = forms.CharField(widget=MarkDownWidget(attrs={'class':'vLargeTextField'}))
|
description = forms.CharField(widget=MarkDownWidget(attrs={'class':'vLargeTextField'}))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = (
|
fields = (
|
||||||
'creator', 'creator_name', 'owner', 'queue', 'subject', 'description',
|
'creator', 'creator_name', 'owner', 'queue', 'subject', 'description',
|
||||||
'priority', 'state', 'cc', 'display_description'
|
'priority', 'state', 'cc', 'display_description'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TicketForm, self).__init__(*args, **kwargs)
|
super(TicketForm, self).__init__(*args, **kwargs)
|
||||||
ticket = kwargs.get('instance', False)
|
ticket = kwargs.get('instance', False)
|
||||||
|
@ -101,7 +101,7 @@ class TicketForm(forms.ModelForm):
|
||||||
description = '<div style="padding-left: 95px;">%s</div>' % description
|
description = '<div style="padding-left: 95px;">%s</div>' % description
|
||||||
widget = SpanWidget(display=description)
|
widget = SpanWidget(display=description)
|
||||||
self.fields['display_description'].widget = widget
|
self.fields['display_description'].widget = widget
|
||||||
|
|
||||||
def clean_description(self):
|
def clean_description(self):
|
||||||
""" clean HTML tags """
|
""" clean HTML tags """
|
||||||
return strip_tags(self.cleaned_data['description'])
|
return strip_tags(self.cleaned_data['description'])
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import orchestra.models.fields
|
import orchestra.models.fields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
||||||
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
||||||
('content', models.TextField(verbose_name='content')),
|
('content', models.TextField(verbose_name='content')),
|
||||||
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
||||||
('author', models.ForeignKey(related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'get_latest_by': 'id',
|
'get_latest_by': 'id',
|
||||||
|
@ -48,9 +49,9 @@ class Migration(migrations.Migration):
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
||||||
('creator', models.ForeignKey(related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||||
('owner', models.ForeignKey(blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
||||||
('queue', models.ForeignKey(blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
('queue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ['-updated_at'],
|
'ordering': ['-updated_at'],
|
||||||
|
@ -60,14 +61,14 @@ class Migration(migrations.Migration):
|
||||||
name='TicketTracker',
|
name='TicketTracker',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
|
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
|
||||||
('ticket', models.ForeignKey(related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
||||||
('user', models.ForeignKey(related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='message',
|
model_name='message',
|
||||||
name='ticket',
|
name='ticket',
|
||||||
field=models.ForeignKey(related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='tickettracker',
|
name='tickettracker',
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.utils.timezone import utc
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('issues', '0001_initial'), ('issues', '0002_auto_20150709_1018'), ('issues', '0003_auto_20160320_1127'), ('issues', '0004_auto_20170528_2011')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Message',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
||||||
|
('content', models.TextField(verbose_name='content')),
|
||||||
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Queue',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128, unique=True, verbose_name='name')),
|
||||||
|
('verbose_name', models.CharField(blank=True, max_length=128, verbose_name='verbose_name')),
|
||||||
|
('default', models.BooleanField(default=False, verbose_name='default')),
|
||||||
|
('notify', orchestra.models.fields.MultiSelectField(blank=True, choices=[('SUPPORT', 'Support tickets'), ('ADMIN', 'Administrative'), ('BILLING', 'Billing'), ('TECH', 'Technical'), ('ADDS', 'Announcements'), ('EMERGENCY', 'Emergency contact')], default=('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY'), help_text='Contacts to notify by email', max_length=256, verbose_name='notify')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Ticket',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('creator_name', models.CharField(blank=True, max_length=256, verbose_name='creator name')),
|
||||||
|
('subject', models.CharField(max_length=256, verbose_name='subject')),
|
||||||
|
('description', models.TextField(verbose_name='description')),
|
||||||
|
('priority', models.CharField(choices=[('HIGH', 'High'), ('MEDIUM', 'Medium'), ('LOW', 'Low')], default='MEDIUM', max_length=32, verbose_name='priority')),
|
||||||
|
('state', models.CharField(choices=[('NEW', 'New'), ('IN_PROGRESS', 'In Progress'), ('RESOLVED', 'Resolved'), ('FEEDBACK', 'Feedback'), ('REJECTED', 'Rejected'), ('CLOSED', 'Closed')], default='NEW', max_length=32, verbose_name='state')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
|
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
||||||
|
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
||||||
|
('queue', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='issues.Queue')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TicketTracker',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='tickettracker',
|
||||||
|
unique_together=set([('ticket', 'user')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='message',
|
||||||
|
name='created_on',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 20, 10, 27, 45, 766388, tzinfo=utc), verbose_name='created at'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='creator',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='queue',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='issues.Queue'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -19,10 +19,10 @@ class Queue(models.Model):
|
||||||
choices=Contact.EMAIL_USAGES,
|
choices=Contact.EMAIL_USAGES,
|
||||||
default=contacts_settings.CONTACTS_DEFAULT_EMAIL_USAGES,
|
default=contacts_settings.CONTACTS_DEFAULT_EMAIL_USAGES,
|
||||||
help_text=_("Contacts to notify by email"))
|
help_text=_("Contacts to notify by email"))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.verbose_name or self.name
|
return self.verbose_name or self.name
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
""" mark as default queue if needed """
|
""" mark as default queue if needed """
|
||||||
existing_default = Queue.objects.filter(default=True)
|
existing_default = Queue.objects.filter(default=True)
|
||||||
|
@ -48,7 +48,7 @@ class Ticket(models.Model):
|
||||||
(MEDIUM, 'Medium'),
|
(MEDIUM, 'Medium'),
|
||||||
(LOW, 'Low'),
|
(LOW, 'Low'),
|
||||||
)
|
)
|
||||||
|
|
||||||
NEW = 'NEW'
|
NEW = 'NEW'
|
||||||
IN_PROGRESS = 'IN_PROGRESS'
|
IN_PROGRESS = 'IN_PROGRESS'
|
||||||
RESOLVED = 'RESOLVED'
|
RESOLVED = 'RESOLVED'
|
||||||
|
@ -63,7 +63,7 @@ class Ticket(models.Model):
|
||||||
(REJECTED, 'Rejected'),
|
(REJECTED, 'Rejected'),
|
||||||
(CLOSED, 'Closed'),
|
(CLOSED, 'Closed'),
|
||||||
)
|
)
|
||||||
|
|
||||||
creator = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("created by"),
|
creator = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("created by"),
|
||||||
related_name='tickets_created', null=True, on_delete=models.SET_NULL)
|
related_name='tickets_created', null=True, on_delete=models.SET_NULL)
|
||||||
creator_name = models.CharField(_("creator name"), max_length=256, blank=True)
|
creator_name = models.CharField(_("creator name"), max_length=256, blank=True)
|
||||||
|
@ -79,15 +79,15 @@ class Ticket(models.Model):
|
||||||
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
||||||
updated_at = models.DateTimeField(_("modified"), auto_now=True)
|
updated_at = models.DateTimeField(_("modified"), auto_now=True)
|
||||||
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), blank=True)
|
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), blank=True)
|
||||||
|
|
||||||
objects = TicketQuerySet.as_manager()
|
objects = TicketQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-updated_at']
|
ordering = ['-updated_at']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.pk)
|
return str(self.pk)
|
||||||
|
|
||||||
def get_notification_emails(self):
|
def get_notification_emails(self):
|
||||||
""" Get emails of the users related to the ticket """
|
""" Get emails of the users related to the ticket """
|
||||||
emails = list(settings.ISSUES_SUPPORT_EMAILS)
|
emails = list(settings.ISSUES_SUPPORT_EMAILS)
|
||||||
|
@ -100,7 +100,7 @@ class Ticket(models.Model):
|
||||||
for message in self.messages.distinct('author'):
|
for message in self.messages.distinct('author'):
|
||||||
emails.append(message.author.email)
|
emails.append(message.author.email)
|
||||||
return set(emails + self.get_cc_emails())
|
return set(emails + self.get_cc_emails())
|
||||||
|
|
||||||
def notify(self, message=None, content=None):
|
def notify(self, message=None, content=None):
|
||||||
""" Send an email to ticket stakeholders notifying an state update """
|
""" Send an email to ticket stakeholders notifying an state update """
|
||||||
emails = self.get_notification_emails()
|
emails = self.get_notification_emails()
|
||||||
|
@ -111,7 +111,7 @@ class Ticket(models.Model):
|
||||||
'ticket_message': message
|
'ticket_message': message
|
||||||
}
|
}
|
||||||
send_email_template(template, context, emails, html=html_template)
|
send_email_template(template, context, emails, html=html_template)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
""" notify stakeholders of new ticket """
|
""" notify stakeholders of new ticket """
|
||||||
new_issue = not self.pk
|
new_issue = not self.pk
|
||||||
|
@ -121,60 +121,60 @@ class Ticket(models.Model):
|
||||||
if new_issue:
|
if new_issue:
|
||||||
# PK should be available for rendering the template
|
# PK should be available for rendering the template
|
||||||
self.notify()
|
self.notify()
|
||||||
|
|
||||||
def is_involved_by(self, user):
|
def is_involved_by(self, user):
|
||||||
""" returns whether user has participated or is referenced on the ticket
|
""" returns whether user has participated or is referenced on the ticket
|
||||||
as owner or member of the group
|
as owner or member of the group
|
||||||
"""
|
"""
|
||||||
return Ticket.objects.filter(pk=self.pk).involved_by(user).exists()
|
return Ticket.objects.filter(pk=self.pk).involved_by(user).exists()
|
||||||
|
|
||||||
def get_cc_emails(self):
|
def get_cc_emails(self):
|
||||||
return self.cc.split(',') if self.cc else []
|
return self.cc.split(',') if self.cc else []
|
||||||
|
|
||||||
def mark_as_read_by(self, user):
|
def mark_as_read_by(self, user):
|
||||||
self.trackers.get_or_create(user=user)
|
self.trackers.get_or_create(user=user)
|
||||||
|
|
||||||
def mark_as_unread_by(self, user):
|
def mark_as_unread_by(self, user):
|
||||||
self.trackers.filter(user=user).delete()
|
self.trackers.filter(user=user).delete()
|
||||||
|
|
||||||
def mark_as_unread(self):
|
def mark_as_unread(self):
|
||||||
self.trackers.all().delete()
|
self.trackers.all().delete()
|
||||||
|
|
||||||
def is_read_by(self, user):
|
def is_read_by(self, user):
|
||||||
return self.trackers.filter(user=user).exists()
|
return self.trackers.filter(user=user).exists()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self):
|
||||||
self.state = Ticket.REJECTED
|
self.state = Ticket.REJECTED
|
||||||
self.save(update_fields=('state', 'updated_at'))
|
self.save(update_fields=('state', 'updated_at'))
|
||||||
|
|
||||||
def resolve(self):
|
def resolve(self):
|
||||||
self.state = Ticket.RESOLVED
|
self.state = Ticket.RESOLVED
|
||||||
self.save(update_fields=('state', 'updated_at'))
|
self.save(update_fields=('state', 'updated_at'))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.state = Ticket.CLOSED
|
self.state = Ticket.CLOSED
|
||||||
self.save(update_fields=('state', 'updated_at'))
|
self.save(update_fields=('state', 'updated_at'))
|
||||||
|
|
||||||
def take(self, user):
|
def take(self, user):
|
||||||
self.owner = user
|
self.owner = user
|
||||||
self.save(update_fields=('state', 'updated_at'))
|
self.save(update_fields=('state', 'updated_at'))
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
|
ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
|
||||||
related_name='messages')
|
verbose_name=_("ticket"), related_name='messages')
|
||||||
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"),
|
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||||
related_name='ticket_messages')
|
verbose_name=_("author"), related_name='ticket_messages')
|
||||||
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
||||||
content = models.TextField(_("content"))
|
content = models.TextField(_("content"))
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'id'
|
get_latest_by = 'id'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "#%i" % self.id
|
return "#%i" % self.id
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
""" notify stakeholders of ticket update """
|
""" notify stakeholders of ticket update """
|
||||||
if not self.pk:
|
if not self.pk:
|
||||||
|
@ -183,7 +183,7 @@ class Message(models.Model):
|
||||||
self.ticket.notify(message=self)
|
self.ticket.notify(message=self)
|
||||||
self.author_name = self.author.get_full_name()
|
self.author_name = self.author.get_full_name()
|
||||||
super(Message, self).save(*args, **kwargs)
|
super(Message, self).save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number(self):
|
def number(self):
|
||||||
return self.ticket.messages.filter(id__lte=self.id).count()
|
return self.ticket.messages.filter(id__lte=self.id).count()
|
||||||
|
@ -191,10 +191,11 @@ class Message(models.Model):
|
||||||
|
|
||||||
class TicketTracker(models.Model):
|
class TicketTracker(models.Model):
|
||||||
""" Keeps track of user read tickets """
|
""" Keeps track of user read tickets """
|
||||||
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers')
|
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
|
||||||
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"),
|
verbose_name=_("ticket"), related_name='trackers')
|
||||||
related_name='ticket_trackers')
|
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("user"), related_name='ticket_trackers')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (
|
unique_together = (
|
||||||
('ticket', 'user'),
|
('ticket', 'user'),
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,8 +23,8 @@ class Migration(migrations.Migration):
|
||||||
('address_name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name', blank=True)),
|
('address_name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name', blank=True)),
|
||||||
('admin_email', models.EmailField(max_length=254, verbose_name='admin email', help_text='Administration email address')),
|
('admin_email', models.EmailField(max_length=254, verbose_name='admin email', help_text='Administration email address')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.')),
|
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.')),
|
||||||
('account', models.ForeignKey(related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
('address_domain', models.ForeignKey(null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
('address_domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('lists', '0001_initial'), ('lists', '0002_auto_20160912_1221'), ('lists', '0003_auto_20160912_1241'), ('lists', '0004_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='List',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Default list address <name>@lists.orchestra.lan', max_length=128, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
|
||||||
|
('address_name', models.CharField(blank=True, max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name')),
|
||||||
|
('admin_email', models.EmailField(help_text='Administration email address', max_length=254, verbose_name='admin email')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('address_domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='domains.Domain', verbose_name='address domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='list',
|
||||||
|
unique_together=set([('address_name', 'address_domain')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='address_domain',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='domains.Domain', verbose_name='address domain'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='address_name',
|
||||||
|
field=models.CharField(blank=True, max_length=52, validators=[orchestra.core.validators.validate_name], verbose_name='address name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@grups.pangea.org', max_length=52, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='address_name',
|
||||||
|
field=models.CharField(blank=True, max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='address name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@grups.pangea.org', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@lists.orchestra.lan', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('lists', '0003_auto_20160912_1241'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@lists.orchestra.lan', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -30,54 +30,54 @@ class List(models.Model):
|
||||||
admin_email = models.EmailField(_("admin email"),
|
admin_email = models.EmailField(_("admin email"),
|
||||||
help_text=_("Administration email address"))
|
help_text=_("Administration email address"))
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='lists')
|
related_name='lists', on_delete=models.CASCADE)
|
||||||
# TODO also admin
|
# TODO also admin
|
||||||
is_active = models.BooleanField(_("active"), default=True,
|
is_active = models.BooleanField(_("active"), default=True,
|
||||||
help_text=_("Designates whether this account should be treated as active. "
|
help_text=_("Designates whether this account should be treated as active. "
|
||||||
"Unselect this instead of deleting accounts."))
|
"Unselect this instead of deleting accounts."))
|
||||||
password = None
|
password = None
|
||||||
|
|
||||||
objects = ListQuerySet.as_manager()
|
objects = ListQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('address_name', 'address_domain')
|
unique_together = ('address_name', 'address_domain')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self):
|
def address(self):
|
||||||
if self.address_name and self.address_domain:
|
if self.address_name and self.address_domain:
|
||||||
return "%s@%s" % (self.address_name, self.address_domain)
|
return "%s@%s" % (self.address_name, self.address_domain)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.is_active and self.account.is_active
|
return self.is_active and self.account.is_active
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.address_name and not self.address_domain_id:
|
if self.address_name and not self.address_domain_id:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'address_domain': _("Domain should be selected for provided address name."),
|
'address_domain': _("Domain should be selected for provided address name."),
|
||||||
})
|
})
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
def get_address_name(self):
|
def get_address_name(self):
|
||||||
return self.address_name or self.name
|
return self.address_name or self.name
|
||||||
|
|
||||||
def get_username(self):
|
def get_username(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
context = {
|
context = {
|
||||||
'name': self.name
|
'name': self.name
|
||||||
|
|
|
@ -12,7 +12,7 @@ from .models import List
|
||||||
|
|
||||||
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = List.address_domain.field.rel.to
|
model = List.address_domain.field.related_model
|
||||||
fields = ('url', 'id', 'name')
|
fields = ('url', 'id', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,14 +26,14 @@ class ListSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
|
||||||
'This value may contain any ascii character except for '
|
'This value may contain any ascii character except for '
|
||||||
' \'/"/\\/ characters.'), 'invalid'),
|
' \'/"/\\/ characters.'), 'invalid'),
|
||||||
])
|
])
|
||||||
|
|
||||||
address_domain = RelatedDomainSerializer(required=False)
|
address_domain = RelatedDomainSerializer(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = List
|
model = List
|
||||||
fields = ('url', 'id', 'name', 'password', 'address_name', 'address_domain', 'admin_email', 'is_active',)
|
fields = ('url', 'id', 'name', 'password', 'address_name', 'address_domain', 'admin_email', 'is_active',)
|
||||||
postonly_fields = ('name', 'password')
|
postonly_fields = ('name', 'password')
|
||||||
|
|
||||||
def validate_address_domain(self, address_name):
|
def validate_address_domain(self, address_name):
|
||||||
if self.instance:
|
if self.instance:
|
||||||
address_domain = address_domain or self.instance.address_domain
|
address_domain = address_domain or self.instance.address_domain
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
import os
|
import os
|
||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
import requests
|
import unittest
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
import requests
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
|
||||||
|
|
||||||
from orchestra.admin.utils import change_url
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.contrib.domains.models import Domain
|
from orchestra.contrib.domains.models import Domain
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Route, Server
|
||||||
from orchestra.utils.sys import sshrun
|
from orchestra.utils.sys import sshrun
|
||||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, snapshot_on_error,
|
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||||
save_response_on_error)
|
save_response_on_error, snapshot_on_error)
|
||||||
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from ... import backends, settings
|
from ... import backends, settings
|
||||||
from ...models import List
|
from ...models import List
|
||||||
|
|
||||||
|
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||||
|
|
||||||
|
|
||||||
class ListMixin(object):
|
class ListMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||||
|
@ -27,12 +29,12 @@ class ListMixin(object):
|
||||||
'orchestra.contrib.domains',
|
'orchestra.contrib.domains',
|
||||||
'orchestra.contrib.lists',
|
'orchestra.contrib.lists',
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ListMixin, self).setUp()
|
super(ListMixin, self).setUp()
|
||||||
self.add_route()
|
self.add_route()
|
||||||
djsettings.DEBUG = True
|
djsettings.DEBUG = True
|
||||||
|
|
||||||
def validate_add(self, name, address=None):
|
def validate_add(self, name, address=None):
|
||||||
sshrun(self.MASTER_SERVER, 'list_members %s' % name, display=False)
|
sshrun(self.MASTER_SERVER, 'list_members %s' % name, display=False)
|
||||||
if not address:
|
if not address:
|
||||||
|
@ -44,11 +46,11 @@ class ListMixin(object):
|
||||||
sshrun(self.MASTER_SERVER,
|
sshrun(self.MASTER_SERVER,
|
||||||
'grep -v ":\|^\s\|^$\|-\|\.\|\s" /var/spool/mail/nobody | base64 -d | grep "%s"'
|
'grep -v ":\|^\s\|^$\|-\|\.\|\s" /var/spool/mail/nobody | base64 -d | grep "%s"'
|
||||||
% request_address, display=False)
|
% request_address, display=False)
|
||||||
|
|
||||||
def validate_login(self, name, password):
|
def validate_login(self, name, password):
|
||||||
url = 'http://%s/cgi-bin/mailman/admin/%s' % (settings.LISTS_DEFAULT_DOMAIN, name)
|
url = 'http://%s/cgi-bin/mailman/admin/%s' % (settings.LISTS_DEFAULT_DOMAIN, name)
|
||||||
self.assertEqual(200, requests.post(url, data={'adminpw': password}).status_code)
|
self.assertEqual(200, requests.post(url, data={'adminpw': password}).status_code)
|
||||||
|
|
||||||
def validate_delete(self, name):
|
def validate_delete(self, name):
|
||||||
context = {
|
context = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
@ -62,7 +64,7 @@ class ListMixin(object):
|
||||||
'grep "^\s*$(domain)s\s*$" %(virtual_domain)s' % context, display=False)
|
'grep "^\s*$(domain)s\s*$" %(virtual_domain)s' % context, display=False)
|
||||||
self.assertRaises(CommandError, sshrun, self.MASTER_SERVER,
|
self.assertRaises(CommandError, sshrun, self.MASTER_SERVER,
|
||||||
'list_lists | grep -i "^\s*%(name)s\s"' % context, display=False)
|
'list_lists | grep -i "^\s*%(name)s\s"' % context, display=False)
|
||||||
|
|
||||||
def subscribe(self, subscribe_address):
|
def subscribe(self, subscribe_address):
|
||||||
msg = MIMEText('')
|
msg = MIMEText('')
|
||||||
msg['To'] = subscribe_address
|
msg['To'] = subscribe_address
|
||||||
|
@ -76,12 +78,12 @@ class ListMixin(object):
|
||||||
server.sendmail(msg['From'], msg['To'], msg.as_string())
|
server.sendmail(msg['From'], msg['To'], msg.as_string())
|
||||||
finally:
|
finally:
|
||||||
server.quit()
|
server.quit()
|
||||||
|
|
||||||
def add_route(self):
|
def add_route(self):
|
||||||
server = Server.objects.create(name=self.MASTER_SERVER)
|
server = Server.objects.create(name=self.MASTER_SERVER)
|
||||||
backend = backends.MailmanController.get_name()
|
backend = backends.MailmanController.get_name()
|
||||||
Route.objects.create(backend=backend, match=True, host=server)
|
Route.objects.create(backend=backend, match=True, host=server)
|
||||||
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -90,7 +92,7 @@ class ListMixin(object):
|
||||||
self.validate_add(name)
|
self.validate_add(name)
|
||||||
self.validate_login(name, password)
|
self.validate_login(name, password)
|
||||||
self.addCleanup(self.delete, name)
|
self.addCleanup(self.delete, name)
|
||||||
|
|
||||||
def test_add_with_address(self):
|
def test_add_with_address(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -102,7 +104,7 @@ class ListMixin(object):
|
||||||
self.addCleanup(self.delete, name)
|
self.addCleanup(self.delete, name)
|
||||||
# Mailman doesn't support changing the address, only the domain
|
# Mailman doesn't support changing the address, only the domain
|
||||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||||
|
|
||||||
def test_change_password(self):
|
def test_change_password(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -113,7 +115,7 @@ class ListMixin(object):
|
||||||
new_password = '@!?%spppP001' % random_ascii(5)
|
new_password = '@!?%spppP001' % random_ascii(5)
|
||||||
self.change_password(name, new_password)
|
self.change_password(name, new_password)
|
||||||
self.validate_login(name, new_password)
|
self.validate_login(name, new_password)
|
||||||
|
|
||||||
def test_change_domain(self):
|
def test_change_domain(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -128,7 +130,7 @@ class ListMixin(object):
|
||||||
address_domain = Domain.objects.create(name=domain_name, account=self.account)
|
address_domain = Domain.objects.create(name=domain_name, account=self.account)
|
||||||
self.update_domain(name, domain_name)
|
self.update_domain(name, domain_name)
|
||||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||||
|
|
||||||
def test_change_address_name(self):
|
def test_change_address_name(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -142,7 +144,7 @@ class ListMixin(object):
|
||||||
address_name = '%s_name' % random_ascii(10)
|
address_name = '%s_name' % random_ascii(10)
|
||||||
self.update_address_name(name, address_name)
|
self.update_address_name(name, address_name)
|
||||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -158,11 +160,12 @@ class ListMixin(object):
|
||||||
self.validate_delete(name)
|
self.validate_delete(name)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||||
class RESTListMixin(ListMixin):
|
class RESTListMixin(ListMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTListMixin, self).setUp()
|
super(RESTListMixin, self).setUp()
|
||||||
self.rest_login()
|
self.rest_login()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def add(self, name, password, admin_email, address_name=None, address_domain=None):
|
def add(self, name, password, admin_email, address_name=None, address_domain=None):
|
||||||
extra = {}
|
extra = {}
|
||||||
|
@ -172,22 +175,22 @@ class RESTListMixin(ListMixin):
|
||||||
'address_domain': self.rest.domains.retrieve(name=address_domain.name).get(),
|
'address_domain': self.rest.domains.retrieve(name=address_domain.name).get(),
|
||||||
})
|
})
|
||||||
self.rest.lists.create(name=name, password=password, admin_email=admin_email, **extra)
|
self.rest.lists.create(name=name, password=password, admin_email=admin_email, **extra)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def delete(self, name):
|
def delete(self, name):
|
||||||
self.rest.lists.retrieve(name=name).delete()
|
self.rest.lists.retrieve(name=name).delete()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def change_password(self, name, password):
|
def change_password(self, name, password):
|
||||||
mail_list = self.rest.lists.retrieve(name=name).get()
|
mail_list = self.rest.lists.retrieve(name=name).get()
|
||||||
mail_list.set_password(password)
|
mail_list.set_password(password)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def update_domain(self, name, domain_name):
|
def update_domain(self, name, domain_name):
|
||||||
mail_list = self.rest.lists.retrieve(name=name).get()
|
mail_list = self.rest.lists.retrieve(name=name).get()
|
||||||
domain = self.rest.domains.retrieve(name=domain_name).get()
|
domain = self.rest.domains.retrieve(name=domain_name).get()
|
||||||
mail_list.update(address_domain=domain)
|
mail_list.update(address_domain=domain)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def update_address_name(self, name, address_name):
|
def update_address_name(self, name, address_name):
|
||||||
mail_list = self.rest.lists.retrieve(name=name).get()
|
mail_list = self.rest.lists.retrieve(name=name).get()
|
||||||
|
@ -198,70 +201,70 @@ class AdminListMixin(ListMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(AdminListMixin, self).setUp()
|
super(AdminListMixin, self).setUp()
|
||||||
self.admin_login()
|
self.admin_login()
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def add(self, name, password, admin_email, address_name=None, address_domain=None):
|
def add(self, name, password, admin_email, address_name=None, address_domain=None):
|
||||||
url = self.live_server_url + reverse('admin:lists_list_add')
|
url = self.live_server_url + reverse('admin:lists_list_add')
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
name_field = self.selenium.find_element_by_id('id_name')
|
name_field = self.selenium.find_element_by_id('id_name')
|
||||||
name_field.send_keys(name)
|
name_field.send_keys(name)
|
||||||
|
|
||||||
password_field = self.selenium.find_element_by_id('id_password1')
|
password_field = self.selenium.find_element_by_id('id_password1')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
password_field = self.selenium.find_element_by_id('id_password2')
|
password_field = self.selenium.find_element_by_id('id_password2')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
|
|
||||||
admin_email_field = self.selenium.find_element_by_id('id_admin_email')
|
admin_email_field = self.selenium.find_element_by_id('id_admin_email')
|
||||||
admin_email_field.send_keys(admin_email)
|
admin_email_field.send_keys(admin_email)
|
||||||
|
|
||||||
if address_name:
|
if address_name:
|
||||||
address_name_field = self.selenium.find_element_by_id('id_address_name')
|
address_name_field = self.selenium.find_element_by_id('id_address_name')
|
||||||
address_name_field.send_keys(address_name)
|
address_name_field.send_keys(address_name)
|
||||||
|
|
||||||
domain = Domain.objects.get(name=address_domain)
|
domain = Domain.objects.get(name=address_domain)
|
||||||
domain_input = self.selenium.find_element_by_id('id_address_domain')
|
domain_input = self.selenium.find_element_by_id('id_address_domain')
|
||||||
domain_select = Select(domain_input)
|
domain_select = Select(domain_input)
|
||||||
domain_select.select_by_value(str(domain.pk))
|
domain_select.select_by_value(str(domain.pk))
|
||||||
|
|
||||||
name_field.submit()
|
name_field.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def delete(self, name):
|
def delete(self, name):
|
||||||
mail_list = List.objects.get(name=name)
|
mail_list = List.objects.get(name=name)
|
||||||
self.admin_delete(mail_list)
|
self.admin_delete(mail_list)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def change_password(self, name, password):
|
def change_password(self, name, password):
|
||||||
mail_list = List.objects.get(name=name)
|
mail_list = List.objects.get(name=name)
|
||||||
self.admin_change_password(mail_list, password)
|
self.admin_change_password(mail_list, password)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def update_domain(self, name, domain_name):
|
def update_domain(self, name, domain_name):
|
||||||
mail_list = List.objects.get(name=name)
|
mail_list = List.objects.get(name=name)
|
||||||
url = self.live_server_url + change_url(mail_list)
|
url = self.live_server_url + change_url(mail_list)
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
domain = Domain.objects.get(name=domain_name)
|
domain = Domain.objects.get(name=domain_name)
|
||||||
domain_input = self.selenium.find_element_by_id('id_address_domain')
|
domain_input = self.selenium.find_element_by_id('id_address_domain')
|
||||||
domain_select = Select(domain_input)
|
domain_select = Select(domain_input)
|
||||||
domain_select.select_by_value(str(domain.pk))
|
domain_select.select_by_value(str(domain.pk))
|
||||||
|
|
||||||
save = self.selenium.find_element_by_name('_save')
|
save = self.selenium.find_element_by_name('_save')
|
||||||
save.submit()
|
save.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def update_address_name(self, name, address_name):
|
def update_address_name(self, name, address_name):
|
||||||
mail_list = List.objects.get(name=name)
|
mail_list = List.objects.get(name=name)
|
||||||
url = self.live_server_url + change_url(mail_list)
|
url = self.live_server_url + change_url(mail_list)
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
address_name_field = self.selenium.find_element_by_id('id_address_name')
|
address_name_field = self.selenium.find_element_by_id('id_address_name')
|
||||||
address_name_field.clear()
|
address_name_field.clear()
|
||||||
address_name_field.send_keys(address_name)
|
address_name_field.send_keys(address_name)
|
||||||
|
|
||||||
save = self.selenium.find_element_by_name('_save')
|
save = self.selenium.find_element_by_name('_save')
|
||||||
save.submit()
|
save.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
|
@ -3,9 +3,10 @@ from urllib.parse import parse_qs
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import F, Count, Value as V
|
from django.db.models import F, Count, Value as V
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
|
from django.utils.html import format_html, format_html_join
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ from .widgets import OpenCustomFilteringOnSelect
|
||||||
class AutoresponseInline(admin.StackedInline):
|
class AutoresponseInline(admin.StackedInline):
|
||||||
model = Autoresponse
|
model = Autoresponse
|
||||||
verbose_name_plural = _("autoresponse")
|
verbose_name_plural = _("autoresponse")
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
if db_field.name == 'subject':
|
if db_field.name == 'subject':
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
|
||||||
|
@ -76,12 +77,13 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
form = MailboxChangeForm
|
form = MailboxChangeForm
|
||||||
list_prefetch_related = ('addresses__domain',)
|
list_prefetch_related = ('addresses__domain',)
|
||||||
actions = (disable, enable, list_accounts)
|
actions = (disable, enable, list_accounts)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(MailboxAdmin, self).__init__(*args, **kwargs)
|
super(MailboxAdmin, self).__init__(*args, **kwargs)
|
||||||
if settings.MAILBOXES_LOCAL_DOMAIN:
|
if settings.MAILBOXES_LOCAL_DOMAIN:
|
||||||
type(self).actions = self.actions + (SendMailboxEmail(),)
|
type(self).actions = self.actions + (SendMailboxEmail(),)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_addresses(self, mailbox):
|
def display_addresses(self, mailbox):
|
||||||
# Get from forwards
|
# Get from forwards
|
||||||
cache = caches.get_request_cache()
|
cache = caches.get_request_cache()
|
||||||
|
@ -93,7 +95,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
qs = qs.values_list('id', 'email', 'forward')
|
qs = qs.values_list('id', 'email', 'forward')
|
||||||
for addr_id, email, mbox in qs:
|
for addr_id, email, mbox in qs:
|
||||||
url = reverse('admin:mailboxes_address_change', args=(addr_id,))
|
url = reverse('admin:mailboxes_address_change', args=(addr_id,))
|
||||||
link = '<a href="%s">%s</a>' % (url, email)
|
link = format_html('<a href="{}">{}</a>', url, email)
|
||||||
try:
|
try:
|
||||||
cached_forwards[mbox].append(link)
|
cached_forwards[mbox].append(link)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -107,32 +109,29 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
addresses = []
|
addresses = []
|
||||||
for addr in mailbox.addresses.all():
|
for addr in mailbox.addresses.all():
|
||||||
url = change_url(addr)
|
url = change_url(addr)
|
||||||
addresses.append('<a href="%s">%s</a>' % (url, addr.email))
|
addresses.append(format_html('<a href="{}">{}</a>', url, addr.email))
|
||||||
return '<br>'.join(addresses+forwards)
|
return '<br>'.join(addresses+forwards)
|
||||||
display_addresses.short_description = _("Addresses")
|
display_addresses.short_description = _("Addresses")
|
||||||
display_addresses.allow_tags = True
|
|
||||||
|
|
||||||
def display_forwards(self, mailbox):
|
def display_forwards(self, mailbox):
|
||||||
forwards = []
|
forwards = mailbox.get_forwards()
|
||||||
for addr in mailbox.get_forwards():
|
return format_html_join(
|
||||||
url = change_url(addr)
|
'<br>', '<a href="{}">{}</a>',
|
||||||
forwards.append('<a href="%s">%s</a>' % (url, addr.email))
|
[(change_url(addr), addr.email) for addr in forwards]
|
||||||
return '<br>'.join(forwards)
|
)
|
||||||
display_forwards.short_description = _("Forward from")
|
display_forwards.short_description = _("Forward from")
|
||||||
display_forwards.allow_tags = True
|
|
||||||
|
@mark_safe
|
||||||
def display_filtering(self, mailbox):
|
def display_filtering(self, mailbox):
|
||||||
""" becacuse of allow_tags = True """
|
|
||||||
return mailbox.get_filtering_display()
|
return mailbox.get_filtering_display()
|
||||||
display_filtering.short_description = _("Filtering")
|
display_filtering.short_description = _("Filtering")
|
||||||
display_filtering.admin_order_field = 'filtering'
|
display_filtering.admin_order_field = 'filtering'
|
||||||
display_filtering.allow_tags = True
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
if db_field.name == 'filtering':
|
if db_field.name == 'filtering':
|
||||||
kwargs['widget'] = OpenCustomFilteringOnSelect()
|
kwargs['widget'] = OpenCustomFilteringOnSelect()
|
||||||
return super(MailboxAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(MailboxAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj)
|
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj)
|
||||||
if obj and obj.filtering == obj.CUSTOM:
|
if obj and obj.filtering == obj.CUSTOM:
|
||||||
|
@ -144,31 +143,31 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
fieldsets = list(copy.deepcopy(fieldsets))
|
fieldsets = list(copy.deepcopy(fieldsets))
|
||||||
fieldsets.pop(-1)
|
fieldsets.pop(-1)
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def get_form(self, *args, **kwargs):
|
def get_form(self, *args, **kwargs):
|
||||||
form = super(MailboxAdmin, self).get_form(*args, **kwargs)
|
form = super(MailboxAdmin, self).get_form(*args, **kwargs)
|
||||||
form.modeladmin = self
|
form.modeladmin = self
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def get_search_results(self, request, queryset, search_term):
|
def get_search_results(self, request, queryset, search_term):
|
||||||
# Remove local domain from the search term if present (implicit local addreç)
|
# Remove local domain from the search term if present (implicit local addreç)
|
||||||
search_term = search_term.replace('@'+settings.MAILBOXES_LOCAL_DOMAIN, '')
|
search_term = search_term.replace('@'+settings.MAILBOXES_LOCAL_DOMAIN, '')
|
||||||
# Split address name from domain in order to support address searching
|
# Split address name from domain in order to support address searching
|
||||||
search_term = search_term.replace('@', ' ')
|
search_term = search_term.replace('@', ' ')
|
||||||
return super(MailboxAdmin, self).get_search_results(request, queryset, search_term)
|
return super(MailboxAdmin, self).get_search_results(request, queryset, search_term)
|
||||||
|
|
||||||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
if not add:
|
if not add:
|
||||||
self.check_unrelated_address(request, obj)
|
self.check_unrelated_address(request, obj)
|
||||||
self.check_matching_address(request, obj)
|
self.check_matching_address(request, obj)
|
||||||
return super(MailboxAdmin, self).render_change_form(
|
return super(MailboxAdmin, self).render_change_form(
|
||||||
request, context, add, change, form_url, obj)
|
request, context, add, change, form_url, obj)
|
||||||
|
|
||||||
def log_addition(self, request, object, *args, **kwargs):
|
def log_addition(self, request, object, *args, **kwargs):
|
||||||
self.check_unrelated_address(request, object)
|
self.check_unrelated_address(request, object)
|
||||||
self.check_matching_address(request, object)
|
self.check_matching_address(request, object)
|
||||||
return super(MailboxAdmin, self).log_addition(request, object, *args, **kwargs)
|
return super(MailboxAdmin, self).log_addition(request, object, *args, **kwargs)
|
||||||
|
|
||||||
def check_matching_address(self, request, obj):
|
def check_matching_address(self, request, obj):
|
||||||
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
if obj.name and local_domain:
|
if obj.name and local_domain:
|
||||||
|
@ -183,7 +182,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
"selecting it makes sense.") % (obj, addr)
|
"selecting it makes sense.") % (obj, addr)
|
||||||
if msg not in (m.message for m in messages.get_messages(request)):
|
if msg not in (m.message for m in messages.get_messages(request)):
|
||||||
self.message_user(request, msg, level=messages.WARNING)
|
self.message_user(request, msg, level=messages.WARNING)
|
||||||
|
|
||||||
def check_unrelated_address(self, request, obj):
|
def check_unrelated_address(self, request, obj):
|
||||||
# Check if there exists an unrelated local Address for this mbox
|
# Check if there exists an unrelated local Address for this mbox
|
||||||
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
|
@ -204,7 +203,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
# Prevent duplication (add_view+continue)
|
# Prevent duplication (add_view+continue)
|
||||||
if msg not in (m.message for m in messages.get_messages(request)):
|
if msg not in (m.message for m in messages.get_messages(request)):
|
||||||
self.message_user(request, msg, level=messages.WARNING)
|
self.message_user(request, msg, level=messages.WARNING)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
""" save hacky mailbox.addresses and local domain clashing """
|
""" save hacky mailbox.addresses and local domain clashing """
|
||||||
if obj.filtering != obj.CUSTOM:
|
if obj.filtering != obj.CUSTOM:
|
||||||
|
@ -217,7 +216,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
elif obj.custom_filtering:
|
elif obj.custom_filtering:
|
||||||
messages.warning(request, msg)
|
messages.warning(request, msg)
|
||||||
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.addresses = form.cleaned_data['addresses']
|
obj.addresses.set(form.cleaned_data['addresses'])
|
||||||
|
|
||||||
|
|
||||||
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
@ -237,39 +236,37 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
filter_horizontal = ['mailboxes']
|
filter_horizontal = ['mailboxes']
|
||||||
form = AddressForm
|
form = AddressForm
|
||||||
list_prefetch_related = ('mailboxes', 'domain')
|
list_prefetch_related = ('mailboxes', 'domain')
|
||||||
|
|
||||||
domain_link = admin_link('domain', order='domain__name')
|
domain_link = admin_link('domain', order='domain__name')
|
||||||
|
|
||||||
def display_email(self, address):
|
def display_email(self, address):
|
||||||
return address.computed_email
|
return address.computed_email
|
||||||
display_email.short_description = _("Email")
|
display_email.short_description = _("Email")
|
||||||
display_email.admin_order_field = 'computed_email'
|
display_email.admin_order_field = 'computed_email'
|
||||||
|
|
||||||
def email_link(self, address):
|
def email_link(self, address):
|
||||||
link = self.domain_link(address)
|
link = self.domain_link(address)
|
||||||
return "%s@%s" % (address.name, link)
|
return format_html("{}@{}", address.name, link)
|
||||||
email_link.short_description = _("Email")
|
email_link.short_description = _("Email")
|
||||||
email_link.allow_tags = True
|
|
||||||
|
|
||||||
def display_mailboxes(self, address):
|
def display_mailboxes(self, address):
|
||||||
boxes = []
|
boxes = address.mailboxes.all()
|
||||||
for mailbox in address.mailboxes.all():
|
return format_html_join(
|
||||||
url = change_url(mailbox)
|
mark_safe('<br>'), '<a href="{}">{}</a>',
|
||||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
|
||||||
return '<br>'.join(boxes)
|
)
|
||||||
display_mailboxes.short_description = _("Mailboxes")
|
display_mailboxes.short_description = _("Mailboxes")
|
||||||
display_mailboxes.allow_tags = True
|
|
||||||
display_mailboxes.admin_order_field = 'mailboxes__count'
|
display_mailboxes.admin_order_field = 'mailboxes__count'
|
||||||
|
|
||||||
def display_all_mailboxes(self, address):
|
def display_all_mailboxes(self, address):
|
||||||
boxes = []
|
boxes = address.get_mailboxes()
|
||||||
for mailbox in address.get_mailboxes():
|
return format_html_join(
|
||||||
url = change_url(mailbox)
|
mark_safe('<br>'), '<a href="{}">{}</a>',
|
||||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
|
||||||
return '<br>'.join(boxes)
|
)
|
||||||
display_all_mailboxes.short_description = _("Mailboxes links")
|
display_all_mailboxes.short_description = _("Mailboxes links")
|
||||||
display_all_mailboxes.allow_tags = True
|
|
||||||
|
@mark_safe
|
||||||
def display_forward(self, address):
|
def display_forward(self, address):
|
||||||
forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()}
|
forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()}
|
||||||
values = []
|
values = []
|
||||||
|
@ -281,14 +278,13 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
values.append(forward)
|
values.append(forward)
|
||||||
return '<br>'.join(values)
|
return '<br>'.join(values)
|
||||||
display_forward.short_description = _("Forward")
|
display_forward.short_description = _("Forward")
|
||||||
display_forward.allow_tags = True
|
|
||||||
display_forward.admin_order_field = 'forward'
|
display_forward.admin_order_field = 'forward'
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
if db_field.name == 'forward':
|
if db_field.name == 'forward':
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
|
||||||
return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def get_fields(self, request, obj=None):
|
def get_fields(self, request, obj=None):
|
||||||
""" Remove mailboxes field when creating address from a popup i.e. from mailbox add form """
|
""" Remove mailboxes field when creating address from a popup i.e. from mailbox add form """
|
||||||
fields = super(AddressAdmin, self).get_fields(request, obj)
|
fields = super(AddressAdmin, self).get_fields(request, obj)
|
||||||
|
@ -297,22 +293,22 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
fields = list(fields)
|
fields = list(fields)
|
||||||
fields.remove('mailboxes')
|
fields.remove('mailboxes')
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(AddressAdmin, self).get_queryset(request)
|
qs = super(AddressAdmin, self).get_queryset(request)
|
||||||
qs = qs.annotate(computed_email=Concat(F('name'), V('@'), F('domain__name')))
|
qs = qs.annotate(computed_email=Concat(F('name'), V('@'), F('domain__name')))
|
||||||
return qs.annotate(Count('mailboxes'))
|
return qs.annotate(Count('mailboxes'))
|
||||||
|
|
||||||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
if not add:
|
if not add:
|
||||||
self.check_matching_mailbox(request, obj)
|
self.check_matching_mailbox(request, obj)
|
||||||
return super(AddressAdmin, self).render_change_form(
|
return super(AddressAdmin, self).render_change_form(
|
||||||
request, context, add, change, form_url, obj)
|
request, context, add, change, form_url, obj)
|
||||||
|
|
||||||
def log_addition(self, request, object, *args, **kwargs):
|
def log_addition(self, request, object, *args, **kwargs):
|
||||||
self.check_matching_mailbox(request, object)
|
self.check_matching_mailbox(request, object)
|
||||||
return super(AddressAdmin, self).log_addition(request, object, *args, **kwargs)
|
return super(AddressAdmin, self).log_addition(request, object, *args, **kwargs)
|
||||||
|
|
||||||
def check_matching_mailbox(self, request, obj):
|
def check_matching_mailbox(self, request, obj):
|
||||||
# Check if new addresse matches with a mbox because of having a local domain
|
# Check if new addresse matches with a mbox because of having a local domain
|
||||||
if obj.name and obj.domain and obj.domain.name == settings.MAILBOXES_LOCAL_DOMAIN:
|
if obj.name and obj.domain and obj.domain.name == settings.MAILBOXES_LOCAL_DOMAIN:
|
||||||
|
|
|
@ -4,7 +4,7 @@ from orchestra.api import router, SetPasswordApiMixin, LogApiMixin
|
||||||
from orchestra.contrib.accounts.api import AccountApiMixin
|
from orchestra.contrib.accounts.api import AccountApiMixin
|
||||||
|
|
||||||
from .models import Address, Mailbox
|
from .models import Address, Mailbox
|
||||||
from .serializers import AddressSerializer, MailboxSerializer
|
from .serializers import AddressSerializer, MailboxSerializer, MailboxWritableSerializer
|
||||||
|
|
||||||
|
|
||||||
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
|
@ -17,6 +17,12 @@ class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets
|
||||||
queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
|
queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
|
||||||
serializer_class = MailboxSerializer
|
serializer_class = MailboxSerializer
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
return self.serializer_class
|
||||||
|
|
||||||
|
return MailboxWritableSerializer
|
||||||
|
|
||||||
|
|
||||||
router.register(r'mailboxes', MailboxViewSet)
|
router.register(r'mailboxes', MailboxViewSet)
|
||||||
router.register(r'addresses', AddressViewSet)
|
router.register(r'addresses', AddressViewSet)
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.mailboxes.validators
|
import orchestra.contrib.mailboxes.validators
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
|
||||||
|
@ -21,8 +22,8 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(verbose_name='name', validators=[orchestra.contrib.mailboxes.validators.validate_emailname], blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64)),
|
('name', models.CharField(verbose_name='name', validators=[orchestra.contrib.mailboxes.validators.validate_emailname], blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64)),
|
||||||
('forward', models.CharField(verbose_name='forward', validators=[orchestra.contrib.mailboxes.validators.validate_forward], blank=True, help_text='Space separated email addresses or mailboxes', max_length=256)),
|
('forward', models.CharField(verbose_name='forward', validators=[orchestra.contrib.mailboxes.validators.validate_forward], blank=True, help_text='Space separated email addresses or mailboxes', max_length=256)),
|
||||||
('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
|
||||||
('domain', models.ForeignKey(to='domains.Domain', related_name='addresses', verbose_name='domain')),
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Domain', related_name='addresses', verbose_name='domain')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'addresses',
|
'verbose_name_plural': 'addresses',
|
||||||
|
@ -35,7 +36,7 @@ class Migration(migrations.Migration):
|
||||||
('subject', models.CharField(verbose_name='subject', max_length=256)),
|
('subject', models.CharField(verbose_name='subject', max_length=256)),
|
||||||
('message', models.TextField(verbose_name='message')),
|
('message', models.TextField(verbose_name='message')),
|
||||||
('enabled', models.BooleanField(verbose_name='enabled', default=False)),
|
('enabled', models.BooleanField(verbose_name='enabled', default=False)),
|
||||||
('address', models.OneToOneField(to='mailboxes.Address', related_name='autoresponse', verbose_name='address')),
|
('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='mailboxes.Address', related_name='autoresponse', verbose_name='address')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -47,7 +48,7 @@ class Migration(migrations.Migration):
|
||||||
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('REDIRECT', 'Archive spam (X-Spam-Score≥9)'), ('DISABLE', 'Disable'), ('REJECT', 'Reject spam (X-Spam-Score≥9)')], max_length=16, default='REDIRECT')),
|
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('REDIRECT', 'Archive spam (X-Spam-Score≥9)'), ('DISABLE', 'Disable'), ('REJECT', 'Reject spam (X-Spam-Score≥9)')], max_length=16, default='REDIRECT')),
|
||||||
('custom_filtering', models.TextField(verbose_name='filtering', validators=[orchestra.contrib.mailboxes.validators.validate_sieve], blank=True, help_text='Arbitrary email filtering in sieve language. This overrides any automatic junk email filtering')),
|
('custom_filtering', models.TextField(verbose_name='filtering', validators=[orchestra.contrib.mailboxes.validators.validate_sieve], blank=True, help_text='Arbitrary email filtering in sieve language. This overrides any automatic junk email filtering')),
|
||||||
('is_active', models.BooleanField(verbose_name='active', default=True)),
|
('is_active', models.BooleanField(verbose_name='active', default=True)),
|
||||||
('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='mailboxes', verbose_name='account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, related_name='mailboxes', verbose_name='account')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'mailboxes',
|
'verbose_name_plural': 'mailboxes',
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.contrib.mailboxes.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('mailboxes', '0001_initial'), ('mailboxes', '0002_auto_20160219_1032'), ('mailboxes', '0003_auto_20170528_2011')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Address',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64, validators=[orchestra.contrib.mailboxes.validators.validate_emailname], verbose_name='name')),
|
||||||
|
('forward', models.CharField(blank=True, help_text='Space separated email addresses or mailboxes', max_length=256, validators=[orchestra.contrib.mailboxes.validators.validate_forward], verbose_name='forward')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='domains.Domain', verbose_name='domain')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'addresses',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Autoresponse',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('subject', models.CharField(max_length=256, verbose_name='subject')),
|
||||||
|
('message', models.TextField(verbose_name='message')),
|
||||||
|
('enabled', models.BooleanField(default=False, verbose_name='enabled')),
|
||||||
|
('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='autoresponse', to='mailboxes.Address', verbose_name='address')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Mailbox',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(db_index=True, help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid mailbox name.')], verbose_name='name')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('DISABLE', 'Disable'), ('REDIRECT', 'Archive spam (Score≥8)'), ('REDIRECT5', 'Archive spam (Score≥5)'), ('REJECT', 'Reject spam (Score≥8)'), ('REJECT5', 'Reject spam (Score≥5)')], default='REDIRECT', max_length=16)),
|
||||||
|
('custom_filtering', models.TextField(blank=True, help_text="Arbitrary email filtering in <a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. This overrides any automatic junk email filtering", validators=[orchestra.contrib.mailboxes.validators.validate_sieve], verbose_name='filtering')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='active')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mailboxes', to=settings.AUTH_USER_MODEL, verbose_name='account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'mailboxes',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='address',
|
||||||
|
name='mailboxes',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='addresses', to='mailboxes.Mailbox', verbose_name='mailboxes'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='address',
|
||||||
|
unique_together=set([('name', 'domain')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -13,7 +13,7 @@ from . import validators, settings
|
||||||
|
|
||||||
class Mailbox(models.Model):
|
class Mailbox(models.Model):
|
||||||
CUSTOM = 'CUSTOM'
|
CUSTOM = 'CUSTOM'
|
||||||
|
|
||||||
name = models.CharField(_("name"), unique=True, db_index=True,
|
name = models.CharField(_("name"), unique=True, db_index=True,
|
||||||
max_length=settings.MAILBOXES_NAME_MAX_LENGTH,
|
max_length=settings.MAILBOXES_NAME_MAX_LENGTH,
|
||||||
help_text=_("Required. %s characters or fewer. Letters, digits and ./-/_ only.") %
|
help_text=_("Required. %s characters or fewer. Letters, digits and ./-/_ only.") %
|
||||||
|
@ -23,7 +23,7 @@ class Mailbox(models.Model):
|
||||||
])
|
])
|
||||||
password = models.CharField(_("password"), max_length=128)
|
password = models.CharField(_("password"), max_length=128)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='mailboxes')
|
related_name='mailboxes', on_delete=models.CASCADE)
|
||||||
filtering = models.CharField(max_length=16,
|
filtering = models.CharField(max_length=16,
|
||||||
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
|
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
|
||||||
choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())])
|
choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())])
|
||||||
|
@ -33,59 +33,59 @@ class Mailbox(models.Model):
|
||||||
"<a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. "
|
"<a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. "
|
||||||
"This overrides any automatic junk email filtering"))
|
"This overrides any automatic junk email filtering"))
|
||||||
is_active = models.BooleanField(_("active"), default=True)
|
is_active = models.BooleanField(_("active"), default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("mailboxes")
|
verbose_name_plural = _("mailboxes")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def active(self):
|
def active(self):
|
||||||
try:
|
try:
|
||||||
return self.is_active and self.account.is_active
|
return self.is_active and self.account.is_active
|
||||||
except type(self).account.field.rel.to.DoesNotExist:
|
except type(self).account.field.related_model.DoesNotExist:
|
||||||
return self.is_active
|
return self.is_active
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
def set_password(self, raw_password):
|
def set_password(self, raw_password):
|
||||||
self.password = make_password(raw_password)
|
self.password = make_password(raw_password)
|
||||||
|
|
||||||
def get_home(self):
|
def get_home(self):
|
||||||
context = {
|
context = {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'username': self.name,
|
'username': self.name,
|
||||||
}
|
}
|
||||||
return os.path.normpath(settings.MAILBOXES_HOME % context)
|
return os.path.normpath(settings.MAILBOXES_HOME % context)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.filtering == self.CUSTOM and not self.custom_filtering:
|
if self.filtering == self.CUSTOM and not self.custom_filtering:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'custom_filtering': _("Custom filtering is selected but not provided.")
|
'custom_filtering': _("Custom filtering is selected but not provided.")
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_filtering(self):
|
def get_filtering(self):
|
||||||
name, content = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering]
|
name, content = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering]
|
||||||
if callable(content):
|
if callable(content):
|
||||||
# Custom filtering
|
# Custom filtering
|
||||||
content = content(self)
|
content = content(self)
|
||||||
return (name, content)
|
return (name, content)
|
||||||
|
|
||||||
def get_local_address(self):
|
def get_local_address(self):
|
||||||
if not settings.MAILBOXES_LOCAL_DOMAIN:
|
if not settings.MAILBOXES_LOCAL_DOMAIN:
|
||||||
raise AttributeError("Mailboxes do not have a defined local address domain.")
|
raise AttributeError("Mailboxes do not have a defined local address domain.")
|
||||||
return '@'.join((self.name, settings.MAILBOXES_LOCAL_DOMAIN))
|
return '@'.join((self.name, settings.MAILBOXES_LOCAL_DOMAIN))
|
||||||
|
|
||||||
def get_forwards(self):
|
def get_forwards(self):
|
||||||
return Address.objects.filter(forward__regex=r'(^|.*\s)%s(\s.*|$)' % self.name)
|
return Address.objects.filter(forward__regex=r'(^|.*\s)%s(\s.*|$)' % self.name)
|
||||||
|
|
||||||
def get_addresses(self):
|
def get_addresses(self):
|
||||||
mboxes = self.addresses.all()
|
mboxes = self.addresses.all()
|
||||||
forwards = self.get_forwards()
|
forwards = self.get_forwards()
|
||||||
|
@ -97,33 +97,33 @@ class Address(models.Model):
|
||||||
validators=[validators.validate_emailname],
|
validators=[validators.validate_emailname],
|
||||||
help_text=_("Address name, left blank for a <i>catch-all</i> address"))
|
help_text=_("Address name, left blank for a <i>catch-all</i> address"))
|
||||||
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
|
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
|
||||||
verbose_name=_("domain"), related_name='addresses')
|
verbose_name=_("domain"), related_name='addresses', on_delete=models.CASCADE)
|
||||||
mailboxes = models.ManyToManyField(Mailbox, verbose_name=_("mailboxes"),
|
mailboxes = models.ManyToManyField(Mailbox, verbose_name=_("mailboxes"),
|
||||||
related_name='addresses', blank=True)
|
related_name='addresses', blank=True)
|
||||||
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
||||||
validators=[validators.validate_forward],
|
validators=[validators.validate_forward],
|
||||||
help_text=_("Space separated email addresses or mailboxes"))
|
help_text=_("Space separated email addresses or mailboxes"))
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='addresses')
|
related_name='addresses', on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("addresses")
|
verbose_name_plural = _("addresses")
|
||||||
unique_together = ('name', 'domain')
|
unique_together = ('name', 'domain')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.email
|
return self.email
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
return "%s@%s" % (self.name, self.domain)
|
return "%s@%s" % (self.name, self.domain)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def destination(self):
|
def destination(self):
|
||||||
destinations = list(self.mailboxes.values_list('name', flat=True))
|
destinations = list(self.mailboxes.values_list('name', flat=True))
|
||||||
if self.forward:
|
if self.forward:
|
||||||
destinations += self.forward.split()
|
destinations += self.forward.split()
|
||||||
return ' '.join(destinations)
|
return ' '.join(destinations)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
errors = defaultdict(list)
|
errors = defaultdict(list)
|
||||||
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
|
@ -149,7 +149,7 @@ class Address(models.Model):
|
||||||
)
|
)
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
def get_forward_mailboxes(self):
|
def get_forward_mailboxes(self):
|
||||||
rm_local_domain = re.compile(r'@%s$' % settings.MAILBOXES_LOCAL_DOMAIN)
|
rm_local_domain = re.compile(r'@%s$' % settings.MAILBOXES_LOCAL_DOMAIN)
|
||||||
mailboxes = []
|
mailboxes = []
|
||||||
|
@ -158,7 +158,7 @@ class Address(models.Model):
|
||||||
if '@' not in forward:
|
if '@' not in forward:
|
||||||
mailboxes.append(forward)
|
mailboxes.append(forward)
|
||||||
return Mailbox.objects.filter(name__in=mailboxes)
|
return Mailbox.objects.filter(name__in=mailboxes)
|
||||||
|
|
||||||
def get_mailboxes(self):
|
def get_mailboxes(self):
|
||||||
for mailbox in self.mailboxes.all():
|
for mailbox in self.mailboxes.all():
|
||||||
yield mailbox
|
yield mailbox
|
||||||
|
@ -168,11 +168,11 @@ class Address(models.Model):
|
||||||
|
|
||||||
class Autoresponse(models.Model):
|
class Autoresponse(models.Model):
|
||||||
address = models.OneToOneField(Address, verbose_name=_("address"),
|
address = models.OneToOneField(Address, verbose_name=_("address"),
|
||||||
related_name='autoresponse')
|
related_name='autoresponse', on_delete=models.CASCADE)
|
||||||
# TODO initial_date
|
# TODO initial_date
|
||||||
subject = models.CharField(_("subject"), max_length=256)
|
subject = models.CharField(_("subject"), max_length=256)
|
||||||
message = models.TextField(_("message"))
|
message = models.TextField(_("message"))
|
||||||
enabled = models.BooleanField(_("enabled"), default=False)
|
enabled = models.BooleanField(_("enabled"), default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.address
|
return self.address
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.db import transaction
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer
|
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer
|
||||||
|
@ -8,17 +9,17 @@ from .models import Mailbox, Address
|
||||||
|
|
||||||
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Address.domain.field.rel.to
|
model = Address.domain.field.related_model
|
||||||
fields = ('url', 'id', 'name')
|
fields = ('url', 'id', 'name')
|
||||||
|
|
||||||
|
|
||||||
class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
domain = RelatedDomainSerializer()
|
domain = RelatedDomainSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Address
|
model = Address
|
||||||
fields = ('url', 'id', 'name', 'domain', 'forward')
|
fields = ('url', 'id', 'name', 'domain', 'forward')
|
||||||
#
|
#
|
||||||
# def from_native(self, data, files=None):
|
# def from_native(self, data, files=None):
|
||||||
# queryset = self.opts.model.objects.filter(account=self.account)
|
# queryset = self.opts.model.objects.filter(account=self.account)
|
||||||
# return get_object_or_404(queryset, name=data['name'])
|
# return get_object_or_404(queryset, name=data['name'])
|
||||||
|
@ -26,7 +27,7 @@ class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedMo
|
||||||
|
|
||||||
class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
|
class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
|
||||||
addresses = RelatedAddressSerializer(many=True, read_only=True)
|
addresses = RelatedAddressSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mailbox
|
model = Mailbox
|
||||||
fields = (
|
fields = (
|
||||||
|
@ -35,6 +36,41 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer
|
||||||
postonly_fields = ('name', 'password')
|
postonly_fields = ('name', 'password')
|
||||||
|
|
||||||
|
|
||||||
|
class AddressRelatedField(serializers.HyperlinkedRelatedField):
|
||||||
|
# Filter addresses by account (user)
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = super().get_queryset()
|
||||||
|
return qs.filter(account=self.context['account'])
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxWritableSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
|
||||||
|
addresses = AddressRelatedField(many=True, view_name='address-detail', queryset=Address.objects.all())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Mailbox
|
||||||
|
fields = (
|
||||||
|
'url', 'id', 'name', 'password', 'filtering', 'custom_filtering', 'addresses', 'is_active'
|
||||||
|
)
|
||||||
|
postonly_fields = ('name', 'password')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['addresses'].context['account'] = self.account
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
addresses = validated_data.pop('addresses', [])
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
instance.addresses.set(addresses)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
addresses = validated_data.pop('addresses', [])
|
||||||
|
instance.addresses.set(addresses)
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mailbox
|
model = Mailbox
|
||||||
|
@ -43,14 +79,29 @@ class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSe
|
||||||
|
|
||||||
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
domain = RelatedDomainSerializer()
|
domain = RelatedDomainSerializer()
|
||||||
mailboxes = RelatedMailboxSerializer(many=True, required=False) #allow_add_remove=True
|
mailboxes = RelatedMailboxSerializer(many=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Address
|
model = Address
|
||||||
fields = ('url', 'id', 'name', 'domain', 'mailboxes', 'forward')
|
fields = ('url', 'id', 'name', 'domain', 'mailboxes', 'forward')
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
attrs = super(AddressSerializer, self).validate(attrs)
|
attrs = super(AddressSerializer, self).validate(attrs)
|
||||||
if not attrs['mailboxes'] and not attrs['forward']:
|
mailboxes = attrs.get('mailboxes', [])
|
||||||
|
forward = attrs.get('forward', '')
|
||||||
|
if not mailboxes and not forward:
|
||||||
raise serializers.ValidationError("A mailbox or forward address should be provided.")
|
raise serializers.ValidationError("A mailbox or forward address should be provided.")
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
mailboxes = validated_data.pop('mailboxes', [])
|
||||||
|
obj = super().create(validated_data)
|
||||||
|
obj.mailboxes.set(mailboxes)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
mailboxes = validated_data.pop('mailboxes', [])
|
||||||
|
instance.mailboxes.set(mailboxes)
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
|
@ -27,7 +27,7 @@ def create_local_address(sender, *args, **kwargs):
|
||||||
mbox = kwargs['instance']
|
mbox = kwargs['instance']
|
||||||
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
if not mbox.pk and local_domain:
|
if not mbox.pk and local_domain:
|
||||||
Domain = Address._meta.get_field('domain').rel.to
|
Domain = Address._meta.get_field('domain').remote_field.model
|
||||||
try:
|
try:
|
||||||
domain = Domain.objects.get(name=local_domain)
|
domain = Domain.objects.get(name=local_domain)
|
||||||
except Domain.DoesNotExist:
|
except Domain.DoesNotExist:
|
||||||
|
|
|
@ -4,13 +4,14 @@ import poplib
|
||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import unittest
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Server, Route
|
||||||
|
@ -21,6 +22,8 @@ from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot
|
||||||
from ... import backends, settings
|
from ... import backends, settings
|
||||||
from ...models import Mailbox
|
from ...models import Mailbox
|
||||||
|
|
||||||
|
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||||
|
|
||||||
|
|
||||||
class MailboxMixin(object):
|
class MailboxMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||||
|
@ -29,21 +32,21 @@ class MailboxMixin(object):
|
||||||
'orchestra.contrib.mails',
|
'orchestra.contrib.mails',
|
||||||
'orchestra.contrib.resources',
|
'orchestra.contrib.resources',
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(MailboxMixin, self).setUp()
|
super(MailboxMixin, self).setUp()
|
||||||
self.add_route()
|
self.add_route()
|
||||||
# clean resource relation from other tests
|
# clean resource relation from other tests
|
||||||
apps.get_app_config('resources').reload_relations()
|
apps.get_app_config('resources').reload_relations()
|
||||||
djsettings.DEBUG = True
|
djsettings.DEBUG = True
|
||||||
|
|
||||||
def add_route(self):
|
def add_route(self):
|
||||||
server = Server.objects.create(name=self.MASTER_SERVER)
|
server = Server.objects.create(name=self.MASTER_SERVER)
|
||||||
backend = backends.PasswdVirtualUserBackend.get_name()
|
backend = backends.RoundcubeIdentityController.get_name()
|
||||||
Route.objects.create(backend=backend, match=True, host=server)
|
Route.objects.create(backend=backend, match=True, host=server)
|
||||||
backend = backends.PostfixAddressController.get_name()
|
backend = backends.PostfixAddressController.get_name()
|
||||||
Route.objects.create(backend=backend, match=True, host=server)
|
Route.objects.create(backend=backend, match=True, host=server)
|
||||||
|
|
||||||
def add_quota_resource(self):
|
def add_quota_resource(self):
|
||||||
Resource.objects.create(
|
Resource.objects.create(
|
||||||
name='disk',
|
name='disk',
|
||||||
|
@ -55,38 +58,38 @@ class MailboxMixin(object):
|
||||||
on_demand=False,
|
on_demand=False,
|
||||||
default_allocation=2000
|
default_allocation=2000
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def add(self):
|
def add(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def add_group(self, username, groupname):
|
def add_group(self, username, groupname):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def login_imap(self, username, password):
|
def login_imap(self, username, password):
|
||||||
mail = imaplib.IMAP4_SSL(self.MASTER_SERVER)
|
mail = imaplib.IMAP4_SSL(self.MASTER_SERVER)
|
||||||
status, msg = mail.login(username, password)
|
status, msg = mail.login(username, password)
|
||||||
self.assertEqual('OK', status)
|
self.assertEqual('OK', status)
|
||||||
self.assertEqual(['Logged in'], msg)
|
self.assertEqual(['Logged in'], msg)
|
||||||
return mail
|
return mail
|
||||||
|
|
||||||
def login_pop3(self, username, password):
|
def login_pop3(self, username, password):
|
||||||
pop = poplib.POP3(self.MASTER_SERVER)
|
pop = poplib.POP3(self.MASTER_SERVER)
|
||||||
pop.user(username)
|
pop.user(username)
|
||||||
pop.pass_(password)
|
pop.pass_(password)
|
||||||
return pop
|
return pop
|
||||||
|
|
||||||
def send_email(self, to, token):
|
def send_email(self, to, token):
|
||||||
msg = MIMEText(token)
|
msg = MIMEText(token)
|
||||||
msg['To'] = to
|
msg['To'] = to
|
||||||
|
@ -100,14 +103,14 @@ class MailboxMixin(object):
|
||||||
server.sendmail(msg['From'], msg['To'], msg.as_string())
|
server.sendmail(msg['From'], msg['To'], msg.as_string())
|
||||||
finally:
|
finally:
|
||||||
server.quit()
|
server.quit()
|
||||||
|
|
||||||
def validate_mailbox(self, username):
|
def validate_mailbox(self, username):
|
||||||
sshrun(self.MASTER_SERVER, "doveadm search -u %s ALL" % username, display=False)
|
sshrun(self.MASTER_SERVER, "doveadm search -u %s ALL" % username, display=False)
|
||||||
|
|
||||||
def validate_email(self, username, token):
|
def validate_email(self, username, token):
|
||||||
home = Mailbox.objects.get(name=username).get_home()
|
home = Mailbox.objects.get(name=username).get_home()
|
||||||
sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False)
|
sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False)
|
||||||
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -115,7 +118,7 @@ class MailboxMixin(object):
|
||||||
self.addCleanup(self.delete, username)
|
self.addCleanup(self.delete, username)
|
||||||
imap = self.login_imap(username, password)
|
imap = self.login_imap(username, password)
|
||||||
self.validate_mailbox(username)
|
self.validate_mailbox(username)
|
||||||
|
|
||||||
def test_change_password(self):
|
def test_change_password(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -125,7 +128,7 @@ class MailboxMixin(object):
|
||||||
new_password = '@!?%spppP001' % random_ascii(5)
|
new_password = '@!?%spppP001' % random_ascii(5)
|
||||||
self.change_password(username, new_password)
|
self.change_password(username, new_password)
|
||||||
imap = self.login_imap(username, new_password)
|
imap = self.login_imap(username, new_password)
|
||||||
|
|
||||||
def test_quota(self):
|
def test_quota(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -139,7 +142,7 @@ class MailboxMixin(object):
|
||||||
imap = self.login_imap(username, password)
|
imap = self.login_imap(username, password)
|
||||||
imap_quota = int(imap.getquotaroot("INBOX")[1][1][0].split(' ')[-1].split(')')[0])
|
imap_quota = int(imap.getquotaroot("INBOX")[1][1][0].split(' ')[-1].split(')')[0])
|
||||||
self.assertEqual(quota*1024, imap_quota)
|
self.assertEqual(quota*1024, imap_quota)
|
||||||
|
|
||||||
def test_send_email(self):
|
def test_send_email(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -155,7 +158,7 @@ class MailboxMixin(object):
|
||||||
server.sendmail(msg['From'], msg['To'], msg.as_string())
|
server.sendmail(msg['From'], msg['To'], msg.as_string())
|
||||||
finally:
|
finally:
|
||||||
server.quit()
|
server.quit()
|
||||||
|
|
||||||
def test_address(self):
|
def test_address(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -168,7 +171,7 @@ class MailboxMixin(object):
|
||||||
token = random_ascii(100)
|
token = random_ascii(100)
|
||||||
self.send_email("%s@%s" % (name, domain), token)
|
self.send_email("%s@%s" % (name, domain), token)
|
||||||
self.validate_email(username, token)
|
self.validate_email(username, token)
|
||||||
|
|
||||||
def test_disable(self):
|
def test_disable(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -178,7 +181,7 @@ class MailboxMixin(object):
|
||||||
imap = self.login_imap(username, password)
|
imap = self.login_imap(username, password)
|
||||||
self.disable(username)
|
self.disable(username)
|
||||||
self.assertRaises(imap.error, self.login_imap, username, password)
|
self.assertRaises(imap.error, self.login_imap, username, password)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%sppppP001' % random_ascii(5)
|
password = '@!?%sppppP001' % random_ascii(5)
|
||||||
|
@ -193,7 +196,7 @@ class MailboxMixin(object):
|
||||||
self.assertRaises(imap.error, self.login_imap, username, password)
|
self.assertRaises(imap.error, self.login_imap, username, password)
|
||||||
self.assertRaises(CommandError,
|
self.assertRaises(CommandError,
|
||||||
sshrun, self.MASTER_SERVER, 'ls %s' % home, display=False)
|
sshrun, self.MASTER_SERVER, 'ls %s' % home, display=False)
|
||||||
|
|
||||||
def test_delete_address(self):
|
def test_delete_address(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -209,14 +212,14 @@ class MailboxMixin(object):
|
||||||
self.delete_address(username)
|
self.delete_address(username)
|
||||||
self.send_email("%s@%s" % (name, domain), token)
|
self.send_email("%s@%s" % (name, domain), token)
|
||||||
self.validate_email(username, token)
|
self.validate_email(username, token)
|
||||||
|
|
||||||
def test_custom_filtering(self):
|
def test_custom_filtering(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
folder = random_ascii(5)
|
folder = random_ascii(5)
|
||||||
filtering = textwrap.dedent("""
|
filtering = textwrap.dedent("""
|
||||||
require "fileinto";
|
require "fileinto";
|
||||||
if true {
|
if true {
|
||||||
fileinto "%s";
|
fileinto "%s";
|
||||||
stop;
|
stop;
|
||||||
}""" % folder)
|
}""" % folder)
|
||||||
|
@ -235,11 +238,12 @@ class MailboxMixin(object):
|
||||||
# TODO test autoreply
|
# TODO test autoreply
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||||
class RESTMailboxMixin(MailboxMixin):
|
class RESTMailboxMixin(MailboxMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTMailboxMixin, self).setUp()
|
super(RESTMailboxMixin, self).setUp()
|
||||||
self.rest_login()
|
self.rest_login()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def add(self, username, password, quota=None, filtering=None):
|
def add(self, username, password, quota=None, filtering=None):
|
||||||
extra = {}
|
extra = {}
|
||||||
|
@ -258,28 +262,28 @@ class RESTMailboxMixin(MailboxMixin):
|
||||||
'custom_filtering': filtering,
|
'custom_filtering': filtering,
|
||||||
})
|
})
|
||||||
self.rest.mailboxes.create(name=username, password=password, **extra)
|
self.rest.mailboxes.create(name=username, password=password, **extra)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def delete(self, username):
|
def delete(self, username):
|
||||||
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
||||||
mailbox.delete()
|
mailbox.delete()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def change_password(self, username, password):
|
def change_password(self, username, password):
|
||||||
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
||||||
mailbox.change_password(password)
|
mailbox.change_password(password)
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def add_address(self, username, name, domain):
|
def add_address(self, username, name, domain):
|
||||||
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
||||||
domain = self.rest.domains.retrieve(name=domain.name).get()
|
domain = self.rest.domains.retrieve(name=domain.name).get()
|
||||||
self.rest.addresses.create(name=name, domain=domain, mailboxes=[mailbox])
|
self.rest.addresses.create(name=name, domain=domain, mailboxes=[mailbox])
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def delete_address(self, username):
|
def delete_address(self, username):
|
||||||
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
||||||
self.rest.addresses.delete()
|
self.rest.addresses.delete()
|
||||||
|
|
||||||
@save_response_on_error
|
@save_response_on_error
|
||||||
def disable(self, username):
|
def disable(self, username):
|
||||||
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
mailbox = self.rest.mailboxes.retrieve(name=username).get()
|
||||||
|
@ -290,30 +294,30 @@ class AdminMailboxMixin(MailboxMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(AdminMailboxMixin, self).setUp()
|
super(AdminMailboxMixin, self).setUp()
|
||||||
self.admin_login()
|
self.admin_login()
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def add(self, username, password, quota=None, filtering=None):
|
def add(self, username, password, quota=None, filtering=None):
|
||||||
url = self.live_server_url + reverse('admin:mailboxes_mailbox_add')
|
url = self.live_server_url + reverse('admin:mailboxes_mailbox_add')
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
# account_input = self.selenium.find_element_by_id('id_account')
|
# account_input = self.selenium.find_element_by_id('id_account')
|
||||||
# account_select = Select(account_input)
|
# account_select = Select(account_input)
|
||||||
# account_select.select_by_value(str(self.account.pk))
|
# account_select.select_by_value(str(self.account.pk))
|
||||||
|
|
||||||
name_field = self.selenium.find_element_by_id('id_name')
|
name_field = self.selenium.find_element_by_id('id_name')
|
||||||
name_field.send_keys(username)
|
name_field.send_keys(username)
|
||||||
|
|
||||||
password_field = self.selenium.find_element_by_id('id_password1')
|
password_field = self.selenium.find_element_by_id('id_password1')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
password_field = self.selenium.find_element_by_id('id_password2')
|
password_field = self.selenium.find_element_by_id('id_password2')
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
|
|
||||||
if quota is not None:
|
if quota is not None:
|
||||||
quota_id = 'id_resources-resourcedata-content_type-object_id-0-allocated'
|
quota_id = 'id_resources-resourcedata-content_type-object_id-0-allocated'
|
||||||
quota_field = self.selenium.find_element_by_id(quota_id)
|
quota_field = self.selenium.find_element_by_id(quota_id)
|
||||||
quota_field.clear()
|
quota_field.clear()
|
||||||
quota_field.send_keys(quota)
|
quota_field.send_keys(quota)
|
||||||
|
|
||||||
if filtering is not None:
|
if filtering is not None:
|
||||||
filtering_input = self.selenium.find_element_by_id('id_filtering')
|
filtering_input = self.selenium.find_element_by_id('id_filtering')
|
||||||
filtering_select = Select(filtering_input)
|
filtering_select = Select(filtering_input)
|
||||||
|
@ -323,45 +327,45 @@ class AdminMailboxMixin(MailboxMixin):
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
filtering_field = self.selenium.find_element_by_id('id_custom_filtering')
|
filtering_field = self.selenium.find_element_by_id('id_custom_filtering')
|
||||||
filtering_field.send_keys(filtering)
|
filtering_field.send_keys(filtering)
|
||||||
|
|
||||||
name_field.submit()
|
name_field.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def delete(self, username):
|
def delete(self, username):
|
||||||
mailbox = Mailbox.objects.get(name=username)
|
mailbox = Mailbox.objects.get(name=username)
|
||||||
self.admin_delete(mailbox)
|
self.admin_delete(mailbox)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def change_password(self, username, password):
|
def change_password(self, username, password):
|
||||||
mailbox = Mailbox.objects.get(name=username)
|
mailbox = Mailbox.objects.get(name=username)
|
||||||
self.admin_change_password(mailbox, password)
|
self.admin_change_password(mailbox, password)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def add_address(self, username, name, domain):
|
def add_address(self, username, name, domain):
|
||||||
url = self.live_server_url + reverse('admin:mailboxes_address_add')
|
url = self.live_server_url + reverse('admin:mailboxes_address_add')
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
name_field = self.selenium.find_element_by_id('id_name')
|
name_field = self.selenium.find_element_by_id('id_name')
|
||||||
name_field.send_keys(name)
|
name_field.send_keys(name)
|
||||||
|
|
||||||
domain_input = self.selenium.find_element_by_id('id_domain')
|
domain_input = self.selenium.find_element_by_id('id_domain')
|
||||||
domain_select = Select(domain_input)
|
domain_select = Select(domain_input)
|
||||||
domain_select.select_by_value(str(domain.pk))
|
domain_select.select_by_value(str(domain.pk))
|
||||||
|
|
||||||
mailboxes = self.selenium.find_element_by_id('id_mailboxes_add_all_link')
|
mailboxes = self.selenium.find_element_by_id('id_mailboxes_add_all_link')
|
||||||
mailboxes.click()
|
mailboxes.click()
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
name_field.submit()
|
name_field.submit()
|
||||||
|
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def delete_address(self, username):
|
def delete_address(self, username):
|
||||||
mailbox = Mailbox.objects.get(name=username)
|
mailbox = Mailbox.objects.get(name=username)
|
||||||
address = mailbox.addresses.get()
|
address = mailbox.addresses.get()
|
||||||
self.admin_delete(address)
|
self.admin_delete(address)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
def disable(self, username):
|
def disable(self, username):
|
||||||
mailbox = Mailbox.objects.get(name=username)
|
mailbox = Mailbox.objects.get(name=username)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,11 @@ import email
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
|
@ -52,20 +54,19 @@ class MessageAdmin(ExtendedModelAdmin):
|
||||||
)
|
)
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
change_view_actions = (last,)
|
change_view_actions = (last,)
|
||||||
|
|
||||||
colored_state = admin_colored('state', colors=COLORS)
|
colored_state = admin_colored('state', colors=COLORS)
|
||||||
created_at_delta = admin_date('created_at')
|
created_at_delta = admin_date('created_at')
|
||||||
last_try_delta = admin_date('last_try')
|
last_try_delta = admin_date('last_try')
|
||||||
|
|
||||||
def display_subject(self, instance):
|
def display_subject(self, instance):
|
||||||
subject = instance.subject
|
subject = instance.subject
|
||||||
if len(subject) > 64:
|
if len(subject) > 64:
|
||||||
return subject[:64] + '…'
|
return mark_safe(subject[:64] + '…')
|
||||||
return subject
|
return subject
|
||||||
display_subject.short_description = _("Subject")
|
display_subject.short_description = _("Subject")
|
||||||
display_subject.admin_order_field = 'subject'
|
display_subject.admin_order_field = 'subject'
|
||||||
display_subject.allow_tags = True
|
|
||||||
|
|
||||||
def display_retries(self, instance):
|
def display_retries(self, instance):
|
||||||
num_logs = instance.logs__count
|
num_logs = instance.logs__count
|
||||||
if num_logs == 1:
|
if num_logs == 1:
|
||||||
|
@ -74,11 +75,10 @@ class MessageAdmin(ExtendedModelAdmin):
|
||||||
else:
|
else:
|
||||||
url = reverse('admin:mailer_smtplog_changelist')
|
url = reverse('admin:mailer_smtplog_changelist')
|
||||||
url += '?&message=%i' % instance.pk
|
url += '?&message=%i' % instance.pk
|
||||||
return '<a href="%s" onclick="return showAddAnotherPopup(this);">%d</a>' % (url, instance.retries)
|
return format_html('<a href="{}" onclick="return showAddAnotherPopup(this);">{}</a>', url, instance.retries)
|
||||||
display_retries.short_description = _("Retries")
|
display_retries.short_description = _("Retries")
|
||||||
display_retries.admin_order_field = 'retries'
|
display_retries.admin_order_field = 'retries'
|
||||||
display_retries.allow_tags = True
|
|
||||||
|
|
||||||
def display_content(self, instance):
|
def display_content(self, instance):
|
||||||
part = email.message_from_string(instance.content)
|
part = email.message_from_string(instance.content)
|
||||||
payload = part.get_payload()
|
payload = part.get_payload()
|
||||||
|
@ -99,22 +99,21 @@ class MessageAdmin(ExtendedModelAdmin):
|
||||||
payload = payload.decode(charset)
|
payload = payload.decode(charset)
|
||||||
if part.get_content_type() == 'text/plain':
|
if part.get_content_type() == 'text/plain':
|
||||||
payload = payload.replace('\n', '<br>').replace(' ', ' ')
|
payload = payload.replace('\n', '<br>').replace(' ', ' ')
|
||||||
return payload
|
return mark_safe(payload)
|
||||||
display_content.short_description = _("Content")
|
display_content.short_description = _("Content")
|
||||||
display_content.allow_tags = True
|
|
||||||
|
|
||||||
def display_full_subject(self, instance):
|
def display_full_subject(self, instance):
|
||||||
return instance.subject
|
return instance.subject
|
||||||
display_full_subject.short_description = _("Subject")
|
display_full_subject.short_description = _("Subject")
|
||||||
|
|
||||||
def display_from(self, instance):
|
def display_from(self, instance):
|
||||||
return instance.from_address
|
return instance.from_address
|
||||||
display_from.short_description = _("From")
|
display_from.short_description = _("From")
|
||||||
|
|
||||||
def display_to(self, instance):
|
def display_to(self, instance):
|
||||||
return instance.to_address
|
return instance.to_address
|
||||||
display_to.short_description = _("To")
|
display_to.short_description = _("To")
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
|
@ -125,16 +124,16 @@ class MessageAdmin(ExtendedModelAdmin):
|
||||||
name='%s_%s_send_pending' % info)
|
name='%s_%s_send_pending' % info)
|
||||||
)
|
)
|
||||||
return urls
|
return urls
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.annotate(Count('logs')).defer('content')
|
return qs.annotate(Count('logs')).defer('content')
|
||||||
|
|
||||||
def send_pending_view(self, request):
|
def send_pending_view(self, request):
|
||||||
task(send_pending).apply_async()
|
task(send_pending).apply_async()
|
||||||
self.message_user(request, _("Pending messages are being sent on the background."))
|
self.message_user(request, _("Pending messages are being sent on the background."))
|
||||||
return redirect('..')
|
return redirect('..')
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
if db_field.name == 'subject':
|
if db_field.name == 'subject':
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
|
||||||
|
@ -148,7 +147,7 @@ class SMTPLogAdmin(admin.ModelAdmin):
|
||||||
list_filter = ('result',)
|
list_filter = ('result',)
|
||||||
fields = ('message_link', 'colored_result', 'date_delta', 'log_message')
|
fields = ('message_link', 'colored_result', 'date_delta', 'log_message')
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
|
|
||||||
message_link = admin_link('message')
|
message_link = admin_link('message')
|
||||||
colored_result = admin_colored('result', colors=COLORS, bold=False)
|
colored_result = admin_colored('result', colors=COLORS, bold=False)
|
||||||
date_delta = admin_date('date')
|
date_delta = admin_date('date')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ class Migration(migrations.Migration):
|
||||||
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
|
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
|
||||||
('date', models.DateTimeField(auto_now_add=True)),
|
('date', models.DateTimeField(auto_now_add=True)),
|
||||||
('log_message', models.TextField()),
|
('log_message', models.TextField()),
|
||||||
('message', models.ForeignKey(to='mailer.Message', editable=False, related_name='logs')),
|
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailer.Message', editable=False, related_name='logs')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('mailer', '0001_initial'), ('mailer', '0002_auto_20150617_1021'), ('mailer', '0003_auto_20150617_1024'), ('mailer', '0004_auto_20150805_1328'), ('mailer', '0005_auto_20160219_1056')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Message',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('state', models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failes')], default='QUEUED', max_length=16, verbose_name='State')),
|
||||||
|
('priority', models.PositiveIntegerField(choices=[(0, 'Critical (not queued)'), (1, 'High'), (2, 'Normal'), (3, 'Low')], default=2, verbose_name='Priority')),
|
||||||
|
('to_address', models.CharField(max_length=256)),
|
||||||
|
('from_address', models.CharField(max_length=256)),
|
||||||
|
('subject', models.CharField(max_length=256, verbose_name='subject')),
|
||||||
|
('content', models.TextField(verbose_name='content')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||||
|
('retries', models.PositiveIntegerField(default=0, verbose_name='retries')),
|
||||||
|
('last_retry', models.DateTimeField(auto_now=True, verbose_name='last try')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SMTPLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
|
||||||
|
('date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('log_message', models.TextField()),
|
||||||
|
('message', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='mailer.Message')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='message',
|
||||||
|
old_name='last_retry',
|
||||||
|
new_name='last_try',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='last_try',
|
||||||
|
field=models.DateTimeField(verbose_name='last try'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='subject',
|
||||||
|
field=models.TextField(verbose_name='subject'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='last_try',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='last try'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failed')], default='QUEUED', max_length=16, verbose_name='State'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='last_try',
|
||||||
|
field=models.DateTimeField(db_index=True, null=True, verbose_name='last try'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='priority',
|
||||||
|
field=models.PositiveIntegerField(choices=[(0, 'Critical (not queued)'), (1, 'High'), (2, 'Normal'), (3, 'Low')], db_index=True, default=2, verbose_name='Priority'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='retries',
|
||||||
|
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='retries'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failed')], db_index=True, default='QUEUED', max_length=16, verbose_name='State'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -15,7 +15,7 @@ class Message(models.Model):
|
||||||
(DEFERRED, _("Deferred")),
|
(DEFERRED, _("Deferred")),
|
||||||
(FAILED, _("Failed")),
|
(FAILED, _("Failed")),
|
||||||
)
|
)
|
||||||
|
|
||||||
CRITICAL = 0
|
CRITICAL = 0
|
||||||
HIGH = 1
|
HIGH = 1
|
||||||
NORMAL = 2
|
NORMAL = 2
|
||||||
|
@ -26,7 +26,7 @@ class Message(models.Model):
|
||||||
(NORMAL, _("Normal")),
|
(NORMAL, _("Normal")),
|
||||||
(LOW, _("Low")),
|
(LOW, _("Low")),
|
||||||
)
|
)
|
||||||
|
|
||||||
state = models.CharField(_("State"), max_length=16, choices=STATES, default=QUEUED,
|
state = models.CharField(_("State"), max_length=16, choices=STATES, default=QUEUED,
|
||||||
db_index=True)
|
db_index=True)
|
||||||
priority = models.PositiveIntegerField(_("Priority"), choices=PRIORITIES, default=NORMAL,
|
priority = models.PositiveIntegerField(_("Priority"), choices=PRIORITIES, default=NORMAL,
|
||||||
|
@ -38,21 +38,21 @@ class Message(models.Model):
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||||
retries = models.PositiveIntegerField(_("retries"), default=0, db_index=True)
|
retries = models.PositiveIntegerField(_("retries"), default=0, db_index=True)
|
||||||
last_try = models.DateTimeField(_("last try"), null=True, db_index=True)
|
last_try = models.DateTimeField(_("last try"), null=True, db_index=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s to %s' % (self.subject, self.to_address)
|
return '%s to %s' % (self.subject, self.to_address)
|
||||||
|
|
||||||
def defer(self):
|
def defer(self):
|
||||||
self.state = self.DEFERRED
|
self.state = self.DEFERRED
|
||||||
# Max tries
|
# Max tries
|
||||||
if self.retries >= len(settings.MAILER_DEFERE_SECONDS):
|
if self.retries >= len(settings.MAILER_DEFERE_SECONDS):
|
||||||
self.state = self.FAILED
|
self.state = self.FAILED
|
||||||
self.save(update_fields=('state',))
|
self.save(update_fields=('state',))
|
||||||
|
|
||||||
def sent(self):
|
def sent(self):
|
||||||
self.state = self.SENT
|
self.state = self.SENT
|
||||||
self.save(update_fields=('state',))
|
self.save(update_fields=('state',))
|
||||||
|
|
||||||
def log(self, error):
|
def log(self, error):
|
||||||
result = SMTPLog.SUCCESS
|
result = SMTPLog.SUCCESS
|
||||||
if error:
|
if error:
|
||||||
|
@ -67,7 +67,7 @@ class SMTPLog(models.Model):
|
||||||
(SUCCESS, _("Success")),
|
(SUCCESS, _("Success")),
|
||||||
(FAILURE, _("Failure")),
|
(FAILURE, _("Failure")),
|
||||||
)
|
)
|
||||||
message = models.ForeignKey(Message, editable=False, related_name='logs')
|
message = models.ForeignKey(Message, editable=False, related_name='logs', on_delete=models.CASCADE)
|
||||||
result = models.CharField(max_length=16, choices=RESULTS, default=SUCCESS)
|
result = models.CharField(max_length=16, choices=RESULTS, default=SUCCESS)
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
log_message = models.TextField()
|
log_message = models.TextField()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -36,19 +37,17 @@ class MiscServiceAdmin(ExtendedModelAdmin):
|
||||||
prepopulated_fields = {'name': ('verbose_name',)}
|
prepopulated_fields = {'name': ('verbose_name',)}
|
||||||
change_readonly_fields = ('name',)
|
change_readonly_fields = ('name',)
|
||||||
actions = (disable, enable)
|
actions = (disable, enable)
|
||||||
|
|
||||||
def display_name(self, misc):
|
def display_name(self, misc):
|
||||||
return '<span title="%s">%s</span>' % (misc.description, misc.name)
|
return format_html('<span title="{}">{}</span>', misc.description, misc.name)
|
||||||
display_name.short_description = _("name")
|
display_name.short_description = _("name")
|
||||||
display_name.allow_tags = True
|
|
||||||
display_name.admin_order_field = 'name'
|
display_name.admin_order_field = 'name'
|
||||||
|
|
||||||
def display_verbose_name(self, misc):
|
def display_verbose_name(self, misc):
|
||||||
return '<span title="%s">%s</span>' % (misc.description, misc.verbose_name)
|
return format_html('<span title="{}">{}</span>', misc.description, misc.verbose_name)
|
||||||
display_verbose_name.short_description = _("verbose name")
|
display_verbose_name.short_description = _("verbose name")
|
||||||
display_verbose_name.allow_tags = True
|
|
||||||
display_verbose_name.admin_order_field = 'verbose_name'
|
display_verbose_name.admin_order_field = 'verbose_name'
|
||||||
|
|
||||||
def num_instances(self, misc):
|
def num_instances(self, misc):
|
||||||
""" return num slivers as a link to slivers changelist view """
|
""" return num slivers as a link to slivers changelist view """
|
||||||
num = misc.instances__count
|
num = misc.instances__count
|
||||||
|
@ -57,11 +56,11 @@ class MiscServiceAdmin(ExtendedModelAdmin):
|
||||||
return mark_safe('<a href="{0}">{1}</a>'.format(url, num))
|
return mark_safe('<a href="{0}">{1}</a>'.format(url, num))
|
||||||
num_instances.short_description = _("Instances")
|
num_instances.short_description = _("Instances")
|
||||||
num_instances.admin_order_field = 'instances__count'
|
num_instances.admin_order_field = 'instances__count'
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(MiscServiceAdmin, self).get_queryset(request)
|
qs = super(MiscServiceAdmin, self).get_queryset(request)
|
||||||
return qs.annotate(models.Count('instances', distinct=True))
|
return qs.annotate(models.Count('instances', distinct=True))
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'description':
|
if db_field.name == 'description':
|
||||||
|
@ -83,21 +82,21 @@ class MiscellaneousAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedMode
|
||||||
actions = (disable, enable)
|
actions = (disable, enable)
|
||||||
plugin_field = 'service'
|
plugin_field = 'service'
|
||||||
plugin = MiscServicePlugin
|
plugin = MiscServicePlugin
|
||||||
|
|
||||||
service_link = admin_link('service')
|
service_link = admin_link('service')
|
||||||
|
|
||||||
def dispaly_active(self, instance):
|
def dispaly_active(self, instance):
|
||||||
return instance.active
|
return instance.active
|
||||||
dispaly_active.short_description = _("Active")
|
dispaly_active.short_description = _("Active")
|
||||||
dispaly_active.boolean = True
|
dispaly_active.boolean = True
|
||||||
dispaly_active.admin_order_field = 'is_active'
|
dispaly_active.admin_order_field = 'is_active'
|
||||||
|
|
||||||
def get_service(self, obj):
|
def get_service(self, obj):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return self.plugin.get(self.plugin_value).related_instance
|
return self.plugin.get(self.plugin_value).related_instance
|
||||||
else:
|
else:
|
||||||
return obj.service
|
return obj.service
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super().get_fieldsets(request, obj)
|
fieldsets = super().get_fieldsets(request, obj)
|
||||||
fields = list(fieldsets[0][1]['fields'])
|
fields = list(fieldsets[0][1]['fields'])
|
||||||
|
@ -110,7 +109,7 @@ class MiscellaneousAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedMode
|
||||||
fields.insert(2, 'identifier')
|
fields.insert(2, 'identifier')
|
||||||
fieldsets[0][1]['fields'] = fields
|
fieldsets[0][1]['fields'] = fields
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
if obj:
|
if obj:
|
||||||
plugin = self.plugin.get(obj.service.name)()
|
plugin = self.plugin.get(obj.service.name)()
|
||||||
|
@ -127,16 +126,16 @@ class MiscellaneousAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedMode
|
||||||
validator = import_class(validator_path)
|
validator = import_class(validator_path)
|
||||||
validator(identifier)
|
validator(identifier)
|
||||||
return identifier
|
return identifier
|
||||||
|
|
||||||
form.clean_identifier = clean_identifier
|
form.clean_identifier = clean_identifier
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'description':
|
if db_field.name == 'description':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
||||||
return super(MiscellaneousAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(MiscellaneousAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
if not change:
|
if not change:
|
||||||
plugin = self.plugin
|
plugin = self.plugin
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import orchestra.models.fields
|
import orchestra.models.fields
|
||||||
|
@ -22,7 +23,7 @@ class Migration(migrations.Migration):
|
||||||
('description', models.TextField(blank=True, verbose_name='description')),
|
('description', models.TextField(blank=True, verbose_name='description')),
|
||||||
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
|
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
|
||||||
('account', models.ForeignKey(related_name='miscellaneous', verbose_name='account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='miscellaneous', verbose_name='account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'miscellaneous',
|
'verbose_name_plural': 'miscellaneous',
|
||||||
|
@ -43,6 +44,6 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='miscellaneous',
|
model_name='miscellaneous',
|
||||||
name='service',
|
name='service',
|
||||||
field=models.ForeignKey(related_name='instances', verbose_name='service', to='miscellaneous.MiscService'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', verbose_name='service', to='miscellaneous.MiscService'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('miscellaneous', '0001_initial'), ('miscellaneous', '0002_auto_20150723_1252')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Miscellaneous',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('identifier', orchestra.models.fields.NullableCharField(help_text='A unique identifier for this service.', max_length=256, null=True, unique=True, verbose_name='identifier')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='description')),
|
||||||
|
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='miscellaneous', to=settings.AUTH_USER_MODEL, verbose_name='account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'miscellaneous',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MiscService',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Raw name used for internal referenciation, i.e. service match definition', max_length=32, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
|
||||||
|
('verbose_name', models.CharField(blank=True, help_text='Human readable name', max_length=256, verbose_name='verbose name')),
|
||||||
|
('description', models.TextField(blank=True, help_text='Optional description', verbose_name='description')),
|
||||||
|
('has_identifier', models.BooleanField(default=True, help_text='Designates if this service has a <b>unique text</b> field that identifies it or not.', verbose_name='has identifier')),
|
||||||
|
('has_amount', models.BooleanField(default=False, help_text='Designates whether this service has <tt>amount</tt> property or not.', verbose_name='has amount')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Whether new instances of this service can be created or not. Unselect this instead of deleting services.', verbose_name='active')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='miscellaneous',
|
||||||
|
name='service',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='miscellaneous.MiscService', verbose_name='service'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='miscellaneous',
|
||||||
|
name='identifier',
|
||||||
|
field=orchestra.models.fields.NullableCharField(db_index=True, help_text='A unique identifier for this service.', max_length=256, null=True, unique=True, verbose_name='identifier'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -22,30 +22,30 @@ class MiscService(models.Model):
|
||||||
is_active = models.BooleanField(_("active"), default=True,
|
is_active = models.BooleanField(_("active"), default=True,
|
||||||
help_text=_("Whether new instances of this service can be created "
|
help_text=_("Whether new instances of this service can be created "
|
||||||
"or not. Unselect this instead of deleting services."))
|
"or not. Unselect this instead of deleting services."))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.verbose_name = self.verbose_name.strip()
|
self.verbose_name = self.verbose_name.strip()
|
||||||
|
|
||||||
def get_verbose_name(self):
|
def get_verbose_name(self):
|
||||||
return self.verbose_name or self.name
|
return self.verbose_name or self.name
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
|
|
||||||
class Miscellaneous(models.Model):
|
class Miscellaneous(models.Model):
|
||||||
service = models.ForeignKey(MiscService, verbose_name=_("service"),
|
service = models.ForeignKey(MiscService, on_delete=models.CASCADE,
|
||||||
related_name='instances')
|
verbose_name=_("service"), related_name='instances')
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='miscellaneous')
|
verbose_name=_("account"), related_name='miscellaneous')
|
||||||
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
|
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
|
||||||
db_index=True, help_text=_("A unique identifier for this service."))
|
db_index=True, help_text=_("A unique identifier for this service."))
|
||||||
description = models.TextField(_("description"), blank=True)
|
description = models.TextField(_("description"), blank=True)
|
||||||
|
@ -53,32 +53,32 @@ class Miscellaneous(models.Model):
|
||||||
is_active = models.BooleanField(_("active"), default=True,
|
is_active = models.BooleanField(_("active"), default=True,
|
||||||
help_text=_("Designates whether this service should be treated as "
|
help_text=_("Designates whether this service should be treated as "
|
||||||
"active. Unselect this instead of deleting services."))
|
"active. Unselect this instead of deleting services."))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("miscellaneous")
|
verbose_name_plural = _("miscellaneous")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.identifier or self.description[:32] or str(self.service)
|
return self.identifier or self.description[:32] or str(self.service)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.is_active and self.service.is_active and self.account.is_active
|
return self.is_active and self.service.is_active and self.account.is_active
|
||||||
|
|
||||||
def get_description(self):
|
def get_description(self):
|
||||||
return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
|
return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save(update_fields=('is_active',))
|
self.save(update_fields=('is_active',))
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def service_class(self):
|
def service_class(self):
|
||||||
return self.service
|
return self.service
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.identifier:
|
if self.identifier:
|
||||||
self.identifier = self.identifier.strip().lower()
|
self.identifier = self.identifier.strip().lower()
|
||||||
|
|
|
@ -15,21 +15,21 @@ class Operation():
|
||||||
MONITOR = 'monitor'
|
MONITOR = 'monitor'
|
||||||
EXCEEDED = 'exceeded'
|
EXCEEDED = 'exceeded'
|
||||||
RECOVERY = 'recovery'
|
RECOVERY = 'recovery'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s.%s(%s)' % (self.backend, self.action, self.instance)
|
return '%s.%s(%s)' % (self.backend, self.action, self.instance)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
""" set() """
|
""" set() """
|
||||||
return hash((self.backend, self.instance, self.action))
|
return hash((self.backend, self.instance, self.action))
|
||||||
|
|
||||||
def __eq__(self, operation):
|
def __eq__(self, operation):
|
||||||
""" set() """
|
""" set() """
|
||||||
return hash(self) == hash(operation)
|
return hash(self) == hash(operation)
|
||||||
|
|
||||||
def __init__(self, backend, instance, action, routes=None):
|
def __init__(self, backend, instance, action, routes=None):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
# instance should maintain any dynamic attribute until backend execution
|
# instance should maintain any dynamic attribute until backend execution
|
||||||
|
@ -37,13 +37,13 @@ class Operation():
|
||||||
self.instance = copy.deepcopy(instance)
|
self.instance = copy.deepcopy(instance)
|
||||||
self.action = action
|
self.action = action
|
||||||
self.routes = routes
|
self.routes = routes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, operations, serialize=False, async=None):
|
def execute(cls, operations, serialize=False, run_async=None):
|
||||||
from . import manager
|
from . import manager
|
||||||
scripts, backend_serialize = manager.generate(operations)
|
scripts, backend_serialize = manager.generate(operations)
|
||||||
return manager.execute(scripts, serialize=(serialize or backend_serialize), async=async)
|
return manager.execute(scripts, serialize=(serialize or backend_serialize), run_async=run_async)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_for_action(cls, instances, action):
|
def create_for_action(cls, instances, action):
|
||||||
if not isinstance(instances, collections.Iterable):
|
if not isinstance(instances, collections.Iterable):
|
||||||
|
@ -56,13 +56,13 @@ class Operation():
|
||||||
cls(backend_cls, instance, action)
|
cls(backend_cls, instance, action)
|
||||||
)
|
)
|
||||||
return operations
|
return operations
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute_action(cls, instances, action):
|
def execute_action(cls, instances, action):
|
||||||
""" instances can be an object or an iterable for batch processing """
|
""" instances can be an object or an iterable for batch processing """
|
||||||
operations = cls.create_for_action(instances, action)
|
operations = cls.create_for_action(instances, action)
|
||||||
return cls.execute(operations)
|
return cls.execute(operations)
|
||||||
|
|
||||||
def preload_context(self):
|
def preload_context(self):
|
||||||
"""
|
"""
|
||||||
Heuristic: Running get_context will prevent most of related objects do not exist errors
|
Heuristic: Running get_context will prevent most of related objects do not exist errors
|
||||||
|
@ -70,7 +70,7 @@ class Operation():
|
||||||
if self.action == self.DELETE:
|
if self.action == self.DELETE:
|
||||||
if hasattr(self.backend, 'get_context'):
|
if hasattr(self.backend, 'get_context'):
|
||||||
self.backend().get_context(self.instance)
|
self.backend().get_context(self.instance)
|
||||||
|
|
||||||
def store(self, log):
|
def store(self, log):
|
||||||
from .models import BackendOperation
|
from .models import BackendOperation
|
||||||
return BackendOperation.objects.create(
|
return BackendOperation.objects.create(
|
||||||
|
@ -79,7 +79,7 @@ class Operation():
|
||||||
instance=self.instance,
|
instance=self.instance,
|
||||||
action=self.action,
|
action=self.action,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, operation, log=None):
|
def load(cls, operation, log=None):
|
||||||
routes = None
|
routes = None
|
||||||
|
@ -88,4 +88,4 @@ class Operation():
|
||||||
(operation.backend, operation.action): AttrDict(host=log.server)
|
(operation.backend, operation.action): AttrDict(host=log.server)
|
||||||
}
|
}
|
||||||
return cls(operation.backend_class, operation.instance, operation.action, routes=routes)
|
return cls(operation.backend_class, operation.instance, operation.action, routes=routes)
|
||||||
|
|
||||||
|
|
|
@ -30,41 +30,40 @@ STATE_COLORS = {
|
||||||
|
|
||||||
class RouteAdmin(ExtendedModelAdmin):
|
class RouteAdmin(ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'async',
|
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'run_async',
|
||||||
'is_active'
|
'is_active'
|
||||||
)
|
)
|
||||||
list_editable = ('host', 'match', 'async', 'is_active')
|
list_editable = ('host', 'match', 'run_async', 'is_active')
|
||||||
list_filter = ('host', 'is_active', 'async', 'backend')
|
list_filter = ('host', 'is_active', 'run_async', 'backend')
|
||||||
list_prefetch_related = ('host',)
|
list_prefetch_related = ('host',)
|
||||||
ordering = ('backend',)
|
ordering = ('backend',)
|
||||||
add_fields = ('backend', 'host', 'match', 'async', 'is_active')
|
add_fields = ('backend', 'host', 'match', 'run_async', 'is_active')
|
||||||
change_form = RouteForm
|
change_form = RouteForm
|
||||||
actions = (orchestrate,)
|
actions = (orchestrate,)
|
||||||
change_view_actions = actions
|
change_view_actions = actions
|
||||||
|
|
||||||
BACKEND_HELP_TEXT = helpers.get_backends_help_text(ServiceBackend.get_backends())
|
BACKEND_HELP_TEXT = helpers.get_backends_help_text(ServiceBackend.get_backends())
|
||||||
DEFAULT_MATCH = {
|
DEFAULT_MATCH = {
|
||||||
backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends()
|
backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends()
|
||||||
}
|
}
|
||||||
|
|
||||||
display_backend = display_plugin_field('backend')
|
display_backend = display_plugin_field('backend')
|
||||||
|
|
||||||
def display_model(self, route):
|
def display_model(self, route):
|
||||||
try:
|
try:
|
||||||
return escape(route.backend_class.model)
|
return route.backend_class.model
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
return mark_safe("<span style='color: red;'>NOT AVAILABLE</span>")
|
||||||
display_model.short_description = _("model")
|
display_model.short_description = _("model")
|
||||||
display_model.allow_tags = True
|
|
||||||
|
@mark_safe
|
||||||
def display_actions(self, route):
|
def display_actions(self, route):
|
||||||
try:
|
try:
|
||||||
return '<br>'.join(route.backend_class.get_actions())
|
return '<br>'.join(route.backend_class.get_actions())
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
||||||
display_actions.short_description = _("actions")
|
display_actions.short_description = _("actions")
|
||||||
display_actions.allow_tags = True
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Provides dynamic help text on backend form field """
|
""" Provides dynamic help text on backend form field """
|
||||||
if db_field.name == 'backend':
|
if db_field.name == 'backend':
|
||||||
|
@ -79,23 +78,23 @@ class RouteAdmin(ExtendedModelAdmin):
|
||||||
request._host_choices_cache = choices = list(field.choices)
|
request._host_choices_cache = choices = list(field.choices)
|
||||||
field.choices = choices
|
field.choices = choices
|
||||||
return field
|
return field
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
""" Include dynamic help text for existing objects """
|
""" Include dynamic help text for existing objects """
|
||||||
form = super(RouteAdmin, self).get_form(request, obj, **kwargs)
|
form = super(RouteAdmin, self).get_form(request, obj, **kwargs)
|
||||||
if obj:
|
if obj:
|
||||||
form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '')
|
form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '')
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def show_orchestration_disabled(self, request):
|
def show_orchestration_disabled(self, request):
|
||||||
if settings.ORCHESTRATION_DISABLE_EXECUTION:
|
if settings.ORCHESTRATION_DISABLE_EXECUTION:
|
||||||
msg = _("Orchestration execution is disabled by <tt>ORCHESTRATION_DISABLE_EXECUTION</tt> setting.")
|
msg = _("Orchestration execution is disabled by <tt>ORCHESTRATION_DISABLE_EXECUTION</tt> setting.")
|
||||||
self.message_user(request, mark_safe(msg), messages.WARNING)
|
self.message_user(request, mark_safe(msg), messages.WARNING)
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
self.show_orchestration_disabled(request)
|
self.show_orchestration_disabled(request)
|
||||||
return super(RouteAdmin, self).changelist_view(request, extra_context)
|
return super(RouteAdmin, self).changelist_view(request, extra_context)
|
||||||
|
|
||||||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||||
self.show_orchestration_disabled(request)
|
self.show_orchestration_disabled(request)
|
||||||
return super(RouteAdmin, self).changeform_view(
|
return super(RouteAdmin, self).changeform_view(
|
||||||
|
@ -108,24 +107,23 @@ class BackendOperationInline(admin.TabularInline):
|
||||||
readonly_fields = ('action', 'instance_link')
|
readonly_fields = ('action', 'instance_link')
|
||||||
extra = 0
|
extra = 0
|
||||||
can_delete = False
|
can_delete = False
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('orchestra/css/hide-inline-id.css',)
|
'all': ('orchestra/css/hide-inline-id.css',)
|
||||||
}
|
}
|
||||||
|
|
||||||
def instance_link(self, operation):
|
def instance_link(self, operation):
|
||||||
link = admin_link('instance')(self, operation)
|
link = admin_link('instance')(self, operation)
|
||||||
if link == '---':
|
if link == '---':
|
||||||
return _("Deleted {0}").format(operation.instance_repr or '-'.join(
|
return _("Deleted {0}").format(operation.instance_repr or '-'.join(
|
||||||
(escape(operation.content_type), escape(operation.object_id))))
|
(escape(operation.content_type), escape(operation.object_id))))
|
||||||
return link
|
return link
|
||||||
instance_link.allow_tags = True
|
|
||||||
instance_link.short_description = _("Instance")
|
instance_link.short_description = _("Instance")
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = super(BackendOperationInline, self).get_queryset(request)
|
queryset = super(BackendOperationInline, self).get_queryset(request)
|
||||||
return queryset.prefetch_related('instance')
|
return queryset.prefetch_related('instance')
|
||||||
|
@ -149,7 +147,7 @@ class BackendLogAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
actions = (retry_backend,)
|
actions = (retry_backend,)
|
||||||
change_view_actions = actions
|
change_view_actions = actions
|
||||||
|
|
||||||
server_link = admin_link('server')
|
server_link = admin_link('server')
|
||||||
display_created = admin_date('created_at', short_description=_("Created"))
|
display_created = admin_date('created_at', short_description=_("Created"))
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
|
@ -157,17 +155,17 @@ class BackendLogAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||||
mono_stdout = display_mono('stdout')
|
mono_stdout = display_mono('stdout')
|
||||||
mono_stderr = display_mono('stderr')
|
mono_stderr = display_mono('stderr')
|
||||||
mono_traceback = display_mono('traceback')
|
mono_traceback = display_mono('traceback')
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('orchestra/css/pygments/github.css',)
|
'all': ('orchestra/css/pygments/github.css',)
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
qs = super(BackendLogAdmin, self).get_queryset(request)
|
qs = super(BackendLogAdmin, self).get_queryset(request)
|
||||||
return qs.select_related('server').defer('script', 'stdout')
|
return qs.select_related('server').defer('script', 'stdout')
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -177,17 +175,15 @@ class ServerAdmin(ExtendedModelAdmin):
|
||||||
list_filter = ('os',)
|
list_filter = ('os',)
|
||||||
actions = (orchestrate,)
|
actions = (orchestrate,)
|
||||||
change_view_actions = actions
|
change_view_actions = actions
|
||||||
|
|
||||||
def display_ping(self, instance):
|
def display_ping(self, instance):
|
||||||
return self._remote_state[instance.pk][0]
|
return mark_safe(self._remote_state[instance.pk][0])
|
||||||
display_ping.short_description = _("Ping")
|
display_ping.short_description = _("Ping")
|
||||||
display_ping.allow_tags = True
|
|
||||||
|
|
||||||
def display_uptime(self, instance):
|
def display_uptime(self, instance):
|
||||||
return self._remote_state[instance.pk][1]
|
return mark_safe(self._remote_state[instance.pk][1])
|
||||||
display_uptime.short_description = _("Uptime")
|
display_uptime.short_description = _("Uptime")
|
||||||
display_uptime.allow_tags = True
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
qs = super(ServerAdmin, self).get_queryset(request)
|
qs = super(ServerAdmin, self).get_queryset(request)
|
||||||
|
|
|
@ -31,7 +31,7 @@ class ServiceMount(plugins.PluginMount):
|
||||||
class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
"""
|
"""
|
||||||
Service management backend base class
|
Service management backend base class
|
||||||
|
|
||||||
It uses the _unit of work_ design principle, which allows bulk operations to
|
It uses the _unit of work_ design principle, which allows bulk operations to
|
||||||
be conviniently supported. Each backend generates the configuration for all
|
be conviniently supported. Each backend generates the configuration for all
|
||||||
the changes of all modified objects, reloading the daemon just once.
|
the changes of all modified objects, reloading the daemon just once.
|
||||||
|
@ -52,15 +52,15 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
# By default backend will not run if actions do not generate insctructions,
|
# By default backend will not run if actions do not generate insctructions,
|
||||||
# If your backend uses prepare() or commit() only then you should set force_empty_action_execution = True
|
# If your backend uses prepare() or commit() only then you should set force_empty_action_execution = True
|
||||||
force_empty_action_execution = False
|
force_empty_action_execution = False
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return type(self).__name__
|
return type(self).__name__
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.head = []
|
self.head = []
|
||||||
self.content = []
|
self.content = []
|
||||||
self.tail = []
|
self.tail = []
|
||||||
|
|
||||||
def __getattribute__(self, attr):
|
def __getattribute__(self, attr):
|
||||||
""" Select head, content or tail section depending on the method name """
|
""" Select head, content or tail section depending on the method name """
|
||||||
IGNORE_ATTRS = (
|
IGNORE_ATTRS = (
|
||||||
|
@ -83,29 +83,29 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
elif attr not in IGNORE_ATTRS and attr in self.actions:
|
elif attr not in IGNORE_ATTRS and attr in self.actions:
|
||||||
self.set_content()
|
self.set_content()
|
||||||
return super(ServiceBackend, self).__getattribute__(attr)
|
return super(ServiceBackend, self).__getattribute__(attr)
|
||||||
|
|
||||||
def set_head(self):
|
def set_head(self):
|
||||||
self.cmd_section = self.head
|
self.cmd_section = self.head
|
||||||
|
|
||||||
def set_tail(self):
|
def set_tail(self):
|
||||||
self.cmd_section = self.tail
|
self.cmd_section = self.tail
|
||||||
|
|
||||||
def set_content(self):
|
def set_content(self):
|
||||||
self.cmd_section = self.content
|
self.cmd_section = self.content
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_actions(cls):
|
def get_actions(cls):
|
||||||
return [ action for action in cls.actions if action in dir(cls) ]
|
return [ action for action in cls.actions if action in dir(cls) ]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_name(cls):
|
def get_name(cls):
|
||||||
return cls.__name__
|
return cls.__name__
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_main(cls, obj):
|
def is_main(cls, obj):
|
||||||
opts = obj._meta
|
opts = obj._meta
|
||||||
return cls.model == '%s.%s' % (opts.app_label, opts.object_name)
|
return cls.model == '%s.%s' % (opts.app_label, opts.object_name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_related(cls, obj):
|
def get_related(cls, obj):
|
||||||
opts = obj._meta
|
opts = obj._meta
|
||||||
|
@ -122,7 +122,7 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
return related.all()
|
return related.all()
|
||||||
return [related]
|
return [related]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backends(cls, instance=None, action=None):
|
def get_backends(cls, instance=None, action=None):
|
||||||
backends = cls.get_plugins()
|
backends = cls.get_plugins()
|
||||||
|
@ -140,15 +140,15 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
if include:
|
if include:
|
||||||
included.append(backend)
|
included.append(backend)
|
||||||
return included
|
return included
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backend(cls, name):
|
def get_backend(cls, name):
|
||||||
return cls.get(name)
|
return cls.get(name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def model_class(cls):
|
def model_class(cls):
|
||||||
return apps.get_model(cls.model)
|
return apps.get_model(cls.model)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scripts(self):
|
def scripts(self):
|
||||||
""" group commands based on their method """
|
""" group commands based on their method """
|
||||||
|
@ -163,12 +163,12 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return list(scripts.items())
|
return list(scripts.items())
|
||||||
|
|
||||||
def get_banner(self):
|
def get_banner(self):
|
||||||
now = timezone.localtime(timezone.now())
|
now = timezone.localtime(timezone.now())
|
||||||
time = now.strftime("%h %d, %Y %I:%M:%S %Z")
|
time = now.strftime("%h %d, %Y %I:%M:%S %Z")
|
||||||
return "Generated by Orchestra at %s" % time
|
return "Generated by Orchestra at %s" % time
|
||||||
|
|
||||||
def create_log(self, server, **kwargs):
|
def create_log(self, server, **kwargs):
|
||||||
from .models import BackendLog
|
from .models import BackendLog
|
||||||
state = BackendLog.RECEIVED
|
state = BackendLog.RECEIVED
|
||||||
|
@ -181,8 +181,8 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
manager = manager.using(using)
|
manager = manager.using(using)
|
||||||
log = manager.create(backend=self.get_name(), state=state, server=server)
|
log = manager.create(backend=self.get_name(), state=state, server=server)
|
||||||
return log
|
return log
|
||||||
|
|
||||||
def execute(self, server, async=False, log=None):
|
def execute(self, server, run_async=False, log=None):
|
||||||
from .models import BackendLog
|
from .models import BackendLog
|
||||||
if log is None:
|
if log is None:
|
||||||
log = self.create_log(server)
|
log = self.create_log(server)
|
||||||
|
@ -190,11 +190,11 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
if run:
|
if run:
|
||||||
scripts = self.scripts
|
scripts = self.scripts
|
||||||
for method, commands in scripts:
|
for method, commands in scripts:
|
||||||
method(log, server, commands, async)
|
method(log, server, commands, run_async)
|
||||||
if log.state != BackendLog.SUCCESS:
|
if log.state != BackendLog.SUCCESS:
|
||||||
break
|
break
|
||||||
return log
|
return log
|
||||||
|
|
||||||
def append(self, *cmd):
|
def append(self, *cmd):
|
||||||
# aggregate commands acording to its execution method
|
# aggregate commands acording to its execution method
|
||||||
if isinstance(cmd[0], str):
|
if isinstance(cmd[0], str):
|
||||||
|
@ -207,10 +207,10 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
self.cmd_section.append((method, [cmd]))
|
self.cmd_section.append((method, [cmd]))
|
||||||
else:
|
else:
|
||||||
self.cmd_section[-1][1].append(cmd)
|
self.cmd_section[-1][1].append(cmd)
|
||||||
|
|
||||||
def get_context(self, obj):
|
def get_context(self, obj):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""
|
"""
|
||||||
hook for executing something at the beging
|
hook for executing something at the beging
|
||||||
|
@ -221,7 +221,7 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
exit_code=0""")
|
exit_code=0""")
|
||||||
)
|
)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
"""
|
"""
|
||||||
hook for executing something at the end
|
hook for executing something at the end
|
||||||
|
@ -235,11 +235,11 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
class ServiceController(ServiceBackend):
|
class ServiceController(ServiceBackend):
|
||||||
actions = ('save', 'delete')
|
actions = ('save', 'delete')
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_verbose_name(cls):
|
def get_verbose_name(cls):
|
||||||
return _("[S] %s") % super(ServiceController, cls).get_verbose_name()
|
return _("[S] %s") % super(ServiceController, cls).get_verbose_name()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backends(cls):
|
def get_backends(cls):
|
||||||
""" filter controller classes """
|
""" filter controller classes """
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from orchestra.forms.widgets import SpanWidget, paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import SpanWidget, PaddingCheckboxSelectMultiple
|
||||||
|
|
||||||
|
|
||||||
class RouteForm(forms.ModelForm):
|
class RouteForm(forms.ModelForm):
|
||||||
|
@ -16,5 +16,5 @@ class RouteForm(forms.ModelForm):
|
||||||
else:
|
else:
|
||||||
self.fields['backend'].widget = SpanWidget()
|
self.fields['backend'].widget = SpanWidget()
|
||||||
actions = backend_class.actions
|
actions = backend_class.actions
|
||||||
self.fields['async_actions'].widget = paddingCheckboxSelectMultiple(45)
|
self.fields['async_actions'].widget = PaddingCheckboxSelectMultiple(45)
|
||||||
self.fields['async_actions'].choices = ((action, action) for action in actions)
|
self.fields['async_actions'].choices = ((action, action) for action in actions)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import textwrap
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.mail import mail_admins
|
from django.core.mail import mail_admins
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ungettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
@ -105,7 +105,7 @@ def get_backend_url(ids):
|
||||||
|
|
||||||
def get_messages(logs):
|
def get_messages(logs):
|
||||||
messages = []
|
messages = []
|
||||||
total, successes, async = 0, 0, 0
|
total, successes, run_async = 0, 0, 0
|
||||||
ids = []
|
ids = []
|
||||||
async_ids = []
|
async_ids = []
|
||||||
for log in logs:
|
for log in logs:
|
||||||
|
@ -118,17 +118,17 @@ def get_messages(logs):
|
||||||
if log.is_success:
|
if log.is_success:
|
||||||
successes += 1
|
successes += 1
|
||||||
elif not log.has_finished:
|
elif not log.has_finished:
|
||||||
async += 1
|
run_async += 1
|
||||||
async_ids.append(log.id)
|
async_ids.append(log.id)
|
||||||
errors = total-successes-async
|
errors = total-successes-run_async
|
||||||
url = get_backend_url(ids)
|
url = get_backend_url(ids)
|
||||||
async_url = get_backend_url(async_ids)
|
async_url = get_backend_url(async_ids)
|
||||||
async_msg = ''
|
async_msg = ''
|
||||||
if async:
|
if run_async:
|
||||||
async_msg = ungettext(
|
async_msg = ungettext(
|
||||||
_('<a href="{async_url}">{name}</a> is running on the background'),
|
_('<a href="{async_url}">{name}</a> is running on the background'),
|
||||||
_('<a href="{async_url}">{async} backends</a> are running on the background'),
|
_('<a href="{async_url}">{run_async} backends</a> are running on the background'),
|
||||||
async)
|
run_async)
|
||||||
if errors:
|
if errors:
|
||||||
if total == 1:
|
if total == 1:
|
||||||
msg = _('<a href="{url}">{name}</a> has fail to execute')
|
msg = _('<a href="{url}">{name}</a> has fail to execute')
|
||||||
|
@ -139,7 +139,7 @@ def get_messages(logs):
|
||||||
errors)
|
errors)
|
||||||
if async_msg:
|
if async_msg:
|
||||||
msg += ', ' + str(async_msg)
|
msg += ', ' + str(async_msg)
|
||||||
msg = msg.format(errors=errors, async=async, async_url=async_url, total=total, url=url,
|
msg = msg.format(errors=errors, run_async=run_async, async_url=async_url, total=total, url=url,
|
||||||
name=log.backend)
|
name=log.backend)
|
||||||
messages.append(('error', msg + '.'))
|
messages.append(('error', msg + '.'))
|
||||||
elif successes:
|
elif successes:
|
||||||
|
@ -158,12 +158,12 @@ def get_messages(logs):
|
||||||
_('<a href="{url}">{total} backends</a> have been executed'),
|
_('<a href="{url}">{total} backends</a> have been executed'),
|
||||||
total)
|
total)
|
||||||
msg = msg.format(
|
msg = msg.format(
|
||||||
total=total, url=url, async_url=async_url, async=async, successes=successes,
|
total=total, url=url, async_url=async_url, run_async=run_async, successes=successes,
|
||||||
name=log.backend
|
name=log.backend
|
||||||
)
|
)
|
||||||
messages.append(('success', msg + '.'))
|
messages.append(('success', msg + '.'))
|
||||||
else:
|
else:
|
||||||
msg = async_msg.format(url=url, async_url=async_url, async=async, name=log.backend)
|
msg = async_msg.format(url=url, async_url=async_url, run_async=run_async, name=log.backend)
|
||||||
messages.append(('success', msg + '.'))
|
messages.append(('success', msg + '.'))
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from orchestra.utils.sys import confirm
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Runs orchestration backends.'
|
help = 'Runs orchestration backends.'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('model', nargs='?',
|
parser.add_argument('model', nargs='?',
|
||||||
help='Label of a model to execute the orchestration.')
|
help='Label of a model to execute the orchestration.')
|
||||||
|
@ -30,8 +30,8 @@ class Command(BaseCommand):
|
||||||
help='List available baclends.')
|
help='List available baclends.')
|
||||||
parser.add_argument('--dry-run', action='store_true', dest='dry', default=False,
|
parser.add_argument('--dry-run', action='store_true', dest='dry', default=False,
|
||||||
help='Only prints scrtipt.')
|
help='Only prints scrtipt.')
|
||||||
|
|
||||||
|
|
||||||
def collect_operations(self, **options):
|
def collect_operations(self, **options):
|
||||||
model = options.get('model')
|
model = options.get('model')
|
||||||
backends = options.get('backends') or set()
|
backends = options.get('backends') or set()
|
||||||
|
@ -66,7 +66,7 @@ class Command(BaseCommand):
|
||||||
model = apps.get_model(*model.split('.'))
|
model = apps.get_model(*model.split('.'))
|
||||||
queryset = model.objects.filter(**kwargs).order_by('id')
|
queryset = model.objects.filter(**kwargs).order_by('id')
|
||||||
querysets = [queryset]
|
querysets = [queryset]
|
||||||
|
|
||||||
operations = OrderedSet()
|
operations = OrderedSet()
|
||||||
route_cache = {}
|
route_cache = {}
|
||||||
for queryset in querysets:
|
for queryset in querysets:
|
||||||
|
@ -88,7 +88,7 @@ class Command(BaseCommand):
|
||||||
result.append(operation)
|
result.append(operation)
|
||||||
operations = result
|
operations = result
|
||||||
return operations
|
return operations
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
list_backends = options.get('list_backends')
|
list_backends = options.get('list_backends')
|
||||||
if list_backends:
|
if list_backends:
|
||||||
|
@ -116,7 +116,7 @@ class Command(BaseCommand):
|
||||||
if not confirm("\n\nAre your sure to execute the previous scripts on %(servers)s (yes/no)? " % context):
|
if not confirm("\n\nAre your sure to execute the previous scripts on %(servers)s (yes/no)? " % context):
|
||||||
return
|
return
|
||||||
if not dry:
|
if not dry:
|
||||||
logs = manager.execute(scripts, serialize=serialize, async=True)
|
logs = manager.execute(scripts, serialize=serialize, run_async=True)
|
||||||
running = list(logs)
|
running = list(logs)
|
||||||
stdout = 0
|
stdout = 0
|
||||||
stderr = 0
|
stderr = 0
|
||||||
|
|
|
@ -97,12 +97,12 @@ def generate(operations):
|
||||||
return scripts, serialize
|
return scripts, serialize
|
||||||
|
|
||||||
|
|
||||||
def execute(scripts, serialize=False, async=None):
|
def execute(scripts, serialize=False, run_async=None):
|
||||||
"""
|
"""
|
||||||
executes the operations on the servers
|
executes the operations on the servers
|
||||||
|
|
||||||
serialize: execute one backend at a time
|
serialize: execute one backend at a time
|
||||||
async: do not join threads (overrides route.async)
|
run_async: do not join threads (overrides route.run_async)
|
||||||
"""
|
"""
|
||||||
if settings.ORCHESTRATION_DISABLE_EXECUTION:
|
if settings.ORCHESTRATION_DISABLE_EXECUTION:
|
||||||
logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION.')
|
logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION.')
|
||||||
|
@ -115,12 +115,12 @@ def execute(scripts, serialize=False, async=None):
|
||||||
route, __, async_action = key
|
route, __, async_action = key
|
||||||
backend, operations = value
|
backend, operations = value
|
||||||
args = (route.host,)
|
args = (route.host,)
|
||||||
if async is None:
|
if run_async is None:
|
||||||
is_async = not serialize and (route.async or async_action)
|
is_async = not serialize and (route.run_async or async_action)
|
||||||
else:
|
else:
|
||||||
is_async = not serialize and (async or async_action)
|
is_async = not serialize and (run_async or async_action)
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'async': is_async,
|
'run_async': is_async,
|
||||||
}
|
}
|
||||||
# we clone the connection just in case we are isolated inside a transaction
|
# we clone the connection just in case we are isolated inside a transaction
|
||||||
with db.clone(model=BackendLog) as handle:
|
with db.clone(model=BackendLog) as handle:
|
||||||
|
|
|
@ -17,7 +17,7 @@ from . import settings
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def Paramiko(backend, log, server, cmds, async=False, paramiko_connections={}):
|
def Paramiko(backend, log, server, cmds, run_async=False, paramiko_connections={}):
|
||||||
"""
|
"""
|
||||||
Executes cmds to remote server using Pramaiko
|
Executes cmds to remote server using Pramaiko
|
||||||
"""
|
"""
|
||||||
|
@ -55,7 +55,7 @@ def Paramiko(backend, log, server, cmds, async=False, paramiko_connections={}):
|
||||||
channel.shutdown_write()
|
channel.shutdown_write()
|
||||||
# Log results
|
# Log results
|
||||||
logger.debug('%s running on %s' % (backend, server))
|
logger.debug('%s running on %s' % (backend, server))
|
||||||
if async:
|
if run_async:
|
||||||
second = False
|
second = False
|
||||||
while True:
|
while True:
|
||||||
# Non-blocking is the secret ingridient in the async sauce
|
# Non-blocking is the secret ingridient in the async sauce
|
||||||
|
@ -78,7 +78,7 @@ def Paramiko(backend, log, server, cmds, async=False, paramiko_connections={}):
|
||||||
else:
|
else:
|
||||||
log.stdout += channel.makefile('rb', -1).read().decode('utf-8')
|
log.stdout += channel.makefile('rb', -1).read().decode('utf-8')
|
||||||
log.stderr += channel.makefile_stderr('rb', -1).read().decode('utf-8')
|
log.stderr += channel.makefile_stderr('rb', -1).read().decode('utf-8')
|
||||||
|
|
||||||
log.exit_code = channel.recv_exit_status()
|
log.exit_code = channel.recv_exit_status()
|
||||||
log.state = log.SUCCESS if log.exit_code == 0 else log.FAILURE
|
log.state = log.SUCCESS if log.exit_code == 0 else log.FAILURE
|
||||||
logger.debug('%s execution state on %s is %s' % (backend, server, log.state))
|
logger.debug('%s execution state on %s is %s' % (backend, server, log.state))
|
||||||
|
@ -97,7 +97,7 @@ def Paramiko(backend, log, server, cmds, async=False, paramiko_connections={}):
|
||||||
channel.close()
|
channel.close()
|
||||||
|
|
||||||
|
|
||||||
def OpenSSH(backend, log, server, cmds, async=False):
|
def OpenSSH(backend, log, server, cmds, run_async=False):
|
||||||
"""
|
"""
|
||||||
Executes cmds to remote server using SSH with connection resuse for maximum performance
|
Executes cmds to remote server using SSH with connection resuse for maximum performance
|
||||||
"""
|
"""
|
||||||
|
@ -110,9 +110,9 @@ def OpenSSH(backend, log, server, cmds, async=False):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
ssh = sshrun(server.get_address(), script, executable=backend.script_executable,
|
ssh = sshrun(server.get_address(), script, executable=backend.script_executable,
|
||||||
persist=True, async=async, silent=True)
|
persist=True, run_async=run_async, silent=True)
|
||||||
logger.debug('%s running on %s' % (backend, server))
|
logger.debug('%s running on %s' % (backend, server))
|
||||||
if async:
|
if run_async:
|
||||||
for state in ssh:
|
for state in ssh:
|
||||||
log.stdout += state.stdout.decode('utf8')
|
log.stdout += state.stdout.decode('utf8')
|
||||||
log.stderr += state.stderr.decode('utf8')
|
log.stderr += state.stderr.decode('utf8')
|
||||||
|
@ -148,7 +148,7 @@ def SSH(*args, **kwargs):
|
||||||
return method(*args, **kwargs)
|
return method(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def Python(backend, log, server, cmds, async=False):
|
def Python(backend, log, server, cmds, run_async=False):
|
||||||
script = ''
|
script = ''
|
||||||
functions = set()
|
functions = set()
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
|
@ -170,7 +170,7 @@ def Python(backend, log, server, cmds, async=False):
|
||||||
log.stdout += line + '\n'
|
log.stdout += line + '\n'
|
||||||
if result:
|
if result:
|
||||||
log.stdout += '# Result: %s\n' % result
|
log.stdout += '# Result: %s\n' % result
|
||||||
if async:
|
if run_async:
|
||||||
log.save(update_fields=('stdout', 'updated_at'))
|
log.save(update_fields=('stdout', 'updated_at'))
|
||||||
except:
|
except:
|
||||||
log.exit_code = 1
|
log.exit_code = 1
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from threading import local
|
from threading import local
|
||||||
|
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
from django.core.urlresolvers import resolve
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models.signals import pre_delete, post_save, m2m_changed
|
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http.response import HttpResponseServerError
|
from django.http.response import HttpResponseServerError
|
||||||
|
from django.urls import resolve
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from orchestra.utils.python import OrderedSet
|
from orchestra.utils.python import OrderedSet
|
||||||
|
|
||||||
from . import manager, Operation
|
from . import Operation, manager
|
||||||
from .helpers import message_user
|
from .helpers import message_user
|
||||||
from .models import BackendLog, BackendOperation
|
from .models import BackendLog, BackendOperation
|
||||||
|
|
||||||
|
@ -35,16 +35,16 @@ def m2m_collector(sender, *args, **kwargs):
|
||||||
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
|
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class OperationsMiddleware(object):
|
class OperationsMiddleware(MiddlewareMixin):
|
||||||
"""
|
"""
|
||||||
Stores all the operations derived from save and delete signals and executes them
|
Stores all the operations derived from save and delete signals and executes them
|
||||||
at the end of the request/response cycle
|
at the end of the request/response cycle
|
||||||
|
|
||||||
It also works as a transaction middleware, making requets to run within an atomic block.
|
It also works as a transaction middleware, making requets to run within an atomic block.
|
||||||
"""
|
"""
|
||||||
# Thread local is used because request object is not available on model signals
|
# Thread local is used because request object is not available on model signals
|
||||||
thread_locals = local()
|
thread_locals = local()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_pending_operations(cls):
|
def get_pending_operations(cls):
|
||||||
# Check if an error poped up before OperationsMiddleware.process_request()
|
# Check if an error poped up before OperationsMiddleware.process_request()
|
||||||
|
@ -54,7 +54,7 @@ class OperationsMiddleware(object):
|
||||||
request.pending_operations = OrderedSet()
|
request.pending_operations = OrderedSet()
|
||||||
return request.pending_operations
|
return request.pending_operations
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_route_cache(cls):
|
def get_route_cache(cls):
|
||||||
""" chache the routes to save sql queries """
|
""" chache the routes to save sql queries """
|
||||||
|
@ -64,7 +64,7 @@ class OperationsMiddleware(object):
|
||||||
request.route_cache = {}
|
request.route_cache = {}
|
||||||
return request.route_cache
|
return request.route_cache
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def collect(cls, action, **kwargs):
|
def collect(cls, action, **kwargs):
|
||||||
""" Collects all pending operations derived from model signals """
|
""" Collects all pending operations derived from model signals """
|
||||||
|
@ -75,26 +75,26 @@ class OperationsMiddleware(object):
|
||||||
kwargs['route_cache'] = cls.get_route_cache()
|
kwargs['route_cache'] = cls.get_route_cache()
|
||||||
instance = kwargs.pop('instance')
|
instance = kwargs.pop('instance')
|
||||||
manager.collect(instance, action, **kwargs)
|
manager.collect(instance, action, **kwargs)
|
||||||
|
|
||||||
def enter_transaction_management(self):
|
def enter_transaction_management(self):
|
||||||
type(self).thread_locals.transaction = transaction.atomic()
|
type(self).thread_locals.transaction = transaction.atomic()
|
||||||
type(self).thread_locals.transaction.__enter__()
|
type(self).thread_locals.transaction.__enter__()
|
||||||
|
|
||||||
def leave_transaction_management(self, exception=None):
|
def leave_transaction_management(self, exception=None):
|
||||||
locals = type(self).thread_locals
|
locals = type(self).thread_locals
|
||||||
if hasattr(locals, 'transaction'):
|
if hasattr(locals, 'transaction'):
|
||||||
# Don't fucking know why sometimes thread_locals does not contain a transaction
|
# Don't fucking know why sometimes thread_locals does not contain a transaction
|
||||||
locals.transaction.__exit__(exception, None, None)
|
locals.transaction.__exit__(exception, None, None)
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
""" Store request on a thread local variable """
|
""" Store request on a thread local variable """
|
||||||
type(self).thread_locals.request = request
|
type(self).thread_locals.request = request
|
||||||
self.enter_transaction_management()
|
self.enter_transaction_management()
|
||||||
|
|
||||||
def process_exception(self, request, exception):
|
def process_exception(self, request, exception):
|
||||||
"""Rolls back the database and leaves transaction management"""
|
"""Rolls back the database and leaves transaction management"""
|
||||||
self.leave_transaction_management(exception)
|
self.leave_transaction_management(exception)
|
||||||
|
|
||||||
def process_response(self, request, response):
|
def process_response(self, request, response):
|
||||||
""" Processes pending backend operations """
|
""" Processes pending backend operations """
|
||||||
if response.status_code != 500:
|
if response.status_code != 500:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import orchestra.models.fields
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
@ -38,8 +39,8 @@ class Migration(migrations.Migration):
|
||||||
('backend', models.CharField(max_length=256, verbose_name='backend')),
|
('backend', models.CharField(max_length=256, verbose_name='backend')),
|
||||||
('action', models.CharField(max_length=64, verbose_name='action')),
|
('action', models.CharField(max_length=64, verbose_name='action')),
|
||||||
('object_id', models.PositiveIntegerField()),
|
('object_id', models.PositiveIntegerField()),
|
||||||
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||||
('log', models.ForeignKey(related_name='operations', to='orchestration.BackendLog')),
|
('log', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operations', to='orchestration.BackendLog')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'Operations',
|
'verbose_name_plural': 'Operations',
|
||||||
|
@ -68,12 +69,12 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='route',
|
model_name='route',
|
||||||
name='host',
|
name='host',
|
||||||
field=models.ForeignKey(to='orchestration.Server', verbose_name='host'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='orchestration.Server', verbose_name='host'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='backendlog',
|
model_name='backendlog',
|
||||||
name='server',
|
name='server',
|
||||||
field=models.ForeignKey(related_name='execution_logs', to='orchestration.Server', verbose_name='server'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution_logs', to='orchestration.Server', verbose_name='server'),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='route',
|
name='route',
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('orchestration', '0001_initial'), ('orchestration', '0002_auto_20150506_1420'), ('orchestration', '0003_auto_20150512_1512'), ('orchestration', '0004_route_async_actions'), ('orchestration', '0005_auto_20150709_1016'), ('orchestration', '0006_auto_20160219_1110'), ('orchestration', '0007_auto_20170528_2011'), ('orchestration', '0008_auto_20190805_1134'), ('orchestration', '0009_rename_route_async_run_async')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BackendLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('backend', models.CharField(max_length=256, verbose_name='backend')),
|
||||||
|
('state', models.CharField(choices=[('RECEIVED', 'RECEIVED'), ('TIMEOUT', 'TIMEOUT'), ('STARTED', 'STARTED'), ('SUCCESS', 'SUCCESS'), ('FAILURE', 'FAILURE'), ('ERROR', 'ERROR'), ('ABORTED', 'ABORTED'), ('REVOKED', 'REVOKED')], default='RECEIVED', max_length=16, verbose_name='state')),
|
||||||
|
('script', models.TextField(verbose_name='script')),
|
||||||
|
('stdout', models.TextField(verbose_name='stdout')),
|
||||||
|
('stderr', models.TextField(verbose_name='stdin')),
|
||||||
|
('traceback', models.TextField(verbose_name='traceback')),
|
||||||
|
('exit_code', models.IntegerField(null=True, verbose_name='exit code')),
|
||||||
|
('task_id', models.CharField(help_text='Celery task ID when used as execution backend', max_length=36, null=True, unique=True, verbose_name='task ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BackendOperation',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('backend', models.CharField(max_length=256, verbose_name='backend')),
|
||||||
|
('action', models.CharField(max_length=64, verbose_name='action')),
|
||||||
|
('object_id', models.PositiveIntegerField(null=True)),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||||
|
('log', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operations', to='orchestration.BackendLog')),
|
||||||
|
('instance_repr', models.CharField(default='', max_length=256, verbose_name='instance representation')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Operations',
|
||||||
|
'verbose_name': 'Operation',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Route',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('backend', models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('OpenVZTraffic', '[M] OpenVZTraffic'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DovecotPostfixPasswdVirtualUserController', '[S] Dovecot-Postfix virtualuser'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailmanController', '[S] Mailman'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PostfixAddressController', '[S] Postfix address'), ('uWSGIPythonController', '[S] Python uWSGI'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend')),
|
||||||
|
('match', models.CharField(blank=True, default='True', help_text='Python expression used for selecting the targe host, <em>instance</em> referes to the current object.', max_length=256, verbose_name='match')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='active')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Server',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Verbose name or hostname of this server.', max_length=256, unique=True, verbose_name='name')),
|
||||||
|
('address', orchestra.models.fields.NullableCharField(blank=True, help_text='Optional IP address or domain name. If blank, name field will be used for address resolution.<br>If the IP address never changes you can set this field and save DNS requests.', max_length=256, null=True, unique=True, validators=[orchestra.core.validators.OrValidator(orchestra.core.validators.validate_ip_address, orchestra.core.validators.validate_hostname)], verbose_name='address')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='description')),
|
||||||
|
('os', models.CharField(choices=[('LINUX', 'Linux')], default='LINUX', max_length=32, verbose_name='operative system')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='route',
|
||||||
|
name='host',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='orchestration.Server', verbose_name='host'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='backendlog',
|
||||||
|
name='server',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution_logs', to='orchestration.Server', verbose_name='server'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='route',
|
||||||
|
name='run_async',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether or not block the request/response cycle waitting this backend to finish its execution. Usually you want slave servers to run asynchronously.'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='route',
|
||||||
|
unique_together=set([('backend', 'host')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='backendlog',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(choices=[('RECEIVED', 'RECEIVED'), ('TIMEOUT', 'TIMEOUT'), ('STARTED', 'STARTED'), ('SUCCESS', 'SUCCESS'), ('FAILURE', 'FAILURE'), ('ERROR', 'ERROR'), ('ABORTED', 'ABORTED'), ('REVOKED', 'REVOKED'), ('NOTHING', 'NOTHING')], default='RECEIVED', max_length=16, verbose_name='state'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='backendlog',
|
||||||
|
name='stderr',
|
||||||
|
field=models.TextField(verbose_name='stderr'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='backend',
|
||||||
|
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('OpenVZTraffic', '[M] OpenVZTraffic'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DovecotPostfixPasswdVirtualUserController', '[S] Dovecot-Postfix virtualuser'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('uWSGIPythonController', '[S] Python uWSGI'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('SyncBind9MasterDomainController', '[S] Sync Bind9 master domain'), ('SyncBind9SlaveDomainController', '[S] Sync Bind9 slave domain'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='route',
|
||||||
|
name='async_actions',
|
||||||
|
field=orchestra.models.fields.MultiSelectField(blank=True, help_text='Specify individual actions to be executed asynchronoulsy.', max_length=256),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='backendlog',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='backend',
|
||||||
|
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('OpenVZTraffic', '[M] OpenVZTraffic'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailScannerSpamRuleController', '[S] MailScanner ruleset'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MoodleController', '[S] Moodle'), ('MoodleWWWRootController', '[S] Moodle WWWRoot (required)'), ('MoodleMuController', '[S] Moodle multisite'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PangeaProxmoxOVZ', '[S] PangeaProxmoxOVZ'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('PostfixRecipientAccessController', '[S] Postfix recipient access'), ('ProxmoxOVZ', '[S] ProxmoxOVZ'), ('uWSGIPythonController', '[S] Python uWSGI'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('SyncBind9MasterDomainController', '[S] Sync Bind9 master domain'), ('SyncBind9SlaveDomainController', '[S] Sync Bind9 slave domain'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressURLController', '[S] WordPress URL'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('OwnCloudController', '[S] ownCloud SaaS'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
|
||||||
|
),
|
||||||
|
migrations.AlterIndexTogether(
|
||||||
|
name='backendoperation',
|
||||||
|
index_together=set([('content_type', 'object_id')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='backend',
|
||||||
|
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('LetsEncryptController', "[S] Let's encrypt!"), ('LxcController', '[S] LxcController'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailScannerSpamRuleController', '[S] MailScanner ruleset'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MoodleController', '[S] Moodle'), ('MoodleWWWRootController', '[S] Moodle WWWRoot (required)'), ('MoodleMuController', '[S] Moodle multisite'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PangeaProxmoxOVZ', '[S] PangeaProxmoxOVZ'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('PostfixRecipientAccessController', '[S] Postfix recipient access'), ('ProxmoxOVZ', '[S] ProxmoxOVZ'), ('uWSGIPythonController', '[S] Python uWSGI'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('SyncBind9MasterDomainController', '[S] Sync Bind9 master domain'), ('SyncBind9SlaveDomainController', '[S] Sync Bind9 slave domain'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressForceSSLController', '[S] WordPress Force SSL'), ('WordPressURLController', '[S] WordPress URL'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('NextCloudController', '[S] nextCloud SaaS'), ('OwnCloudController', '[S] ownCloud SaaS'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='host',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='routes', to='orchestration.Server', verbose_name='host'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='backend',
|
||||||
|
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('LetsEncryptController', "[S] Let's encrypt!"), ('LxcController', '[S] LxcController'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailScannerSpamRuleController', '[S] MailScanner ruleset'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MoodleController', '[S] Moodle'), ('MoodleWWWRootController', '[S] Moodle WWWRoot (required)'), ('MoodleMuController', '[S] Moodle multisite'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PangeaProxmoxOVZ', '[S] PangeaProxmoxOVZ'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('PostfixRecipientAccessController', '[S] Postfix recipient access'), ('ProxmoxOVZ', '[S] ProxmoxOVZ'), ('uWSGIPythonController', '[S] Python uWSGI'), ('RoundcubeIdentityController', '[S] Roundcube Identity Controller'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('SyncBind9MasterDomainController', '[S] Sync Bind9 master domain'), ('SyncBind9SlaveDomainController', '[S] Sync Bind9 slave domain'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressForceSSLController', '[S] WordPress Force SSL'), ('WordPressURLController', '[S] WordPress URL'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('NextCloudController', '[S] nextCloud SaaS'), ('OwnCloudController', '[S] ownCloud SaaS'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='backend',
|
||||||
|
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('LetsEncryptController', "[S] Let's encrypt!"), ('LxcController', '[S] LxcController'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MoodleController', '[S] Moodle'), ('MoodleWWWRootController', '[S] Moodle WWWRoot (required)'), ('MoodleMuController', '[S] Moodle multisite'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('ProxmoxOVZ', '[S] ProxmoxOVZ'), ('uWSGIPythonController', '[S] Python uWSGI'), ('RoundcubeIdentityController', '[S] Roundcube Identity Controller'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressForceSSLController', '[S] WordPress Force SSL'), ('WordPressURLController', '[S] WordPress URL'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('NextCloudController', '[S] nextCloud SaaS'), ('OwnCloudController', '[S] ownCloud SaaS'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('orchestration', '0008_auto_20190805_1134'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='route',
|
||||||
|
old_name='async',
|
||||||
|
new_name='run_async',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='backend',
|
||||||
|
field=models.CharField(choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('ApacheTrafficByName', '[M] ApacheTrafficByName'), ('DokuWikiMuTraffic', '[M] DokuWiki MU Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('ProxmoxOpenVZTraffic', '[M] ProxmoxOpenVZTraffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic'), ('WordpressMuTraffic', '[M] Wordpress MU Traffic'), ('NextCloudDiskQuota', '[M] nextCloud SaaS Disk Quota'), ('NextcloudTraffic', '[M] nextCloud SaaS Traffic'), ('OwnCloudDiskQuota', '[M] ownCloud SaaS Disk Quota'), ('OwncloudTraffic', '[M] ownCloud SaaS Traffic'), ('PhpListTraffic', '[M] phpList SaaS Traffic'), ('Apache2Controller', '[S] Apache 2'), ('BSCWController', '[S] BSCW SaaS'), ('Bind9MasterDomainController', '[S] Bind9 master domain'), ('Bind9SlaveDomainController', '[S] Bind9 slave domain'), ('DokuWikiMuController', '[S] DokuWiki multisite'), ('DrupalMuController', '[S] Drupal multisite'), ('GitLabSaaSController', '[S] GitLab SaaS'), ('LetsEncryptController', "[S] Let's encrypt!"), ('LxcController', '[S] LxcController'), ('AutoresponseController', '[S] Mail autoresponse'), ('MailmanController', '[S] Mailman'), ('MailmanVirtualDomainController', '[S] Mailman virtdomain-only'), ('MoodleController', '[S] Moodle'), ('MoodleWWWRootController', '[S] Moodle WWWRoot (required)'), ('MoodleMuController', '[S] Moodle multisite'), ('MySQLController', '[S] MySQL database'), ('MySQLUserController', '[S] MySQL user'), ('PHPController', '[S] PHP FPM/FCGID'), ('PostfixAddressController', '[S] Postfix address'), ('PostfixAddressVirtualDomainController', '[S] Postfix address virtdomain-only'), ('ProxmoxOVZ', '[S] ProxmoxOVZ'), ('uWSGIPythonController', '[S] Python uWSGI'), ('RoundcubeIdentityController', '[S] Roundcube Identity Controller'), ('StaticController', '[S] Static'), ('SymbolicLinkController', '[S] Symbolic link webapp'), ('UNIXUserMaildirController', '[S] UNIX maildir user'), ('UNIXUserController', '[S] UNIX user'), ('WebalizerAppController', '[S] Webalizer App'), ('WebalizerController', '[S] Webalizer Content'), ('WordPressForceSSLController', '[S] WordPress Force SSL'), ('WordPressURLController', '[S] WordPress URL'), ('WordPressController', '[S] Wordpress'), ('WordpressMuController', '[S] Wordpress multisite'), ('NextCloudController', '[S] nextCloud SaaS'), ('OwnCloudController', '[S] ownCloud SaaS'), ('PhpListSaaSController', '[S] phpList SaaS')], max_length=256, verbose_name='backend'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -33,26 +33,27 @@ class Server(models.Model):
|
||||||
os = models.CharField(_("operative system"), max_length=32,
|
os = models.CharField(_("operative system"), max_length=32,
|
||||||
choices=settings.ORCHESTRATION_OS_CHOICES,
|
choices=settings.ORCHESTRATION_OS_CHOICES,
|
||||||
default=settings.ORCHESTRATION_DEFAULT_OS)
|
default=settings.ORCHESTRATION_DEFAULT_OS)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name or str(self.address)
|
return self.name or str(self.address)
|
||||||
|
|
||||||
def get_address(self):
|
def get_address(self):
|
||||||
if self.address:
|
if self.address:
|
||||||
return self.address
|
return self.address
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_ip(self):
|
def get_ip(self):
|
||||||
address = self.get_address()
|
address = self.get_address()
|
||||||
try:
|
try:
|
||||||
return validate_ip_address(address)
|
return validate_ip_address(address)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return socket.gethostbyname(self.name)
|
return socket.gethostbyname(self.name)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.name = self.name.strip()
|
self.name = self.name.strip()
|
||||||
self.address = self.address.strip()
|
if self.address:
|
||||||
if self.name and not self.address:
|
self.address = self.address.strip()
|
||||||
|
elif self.name:
|
||||||
validate = OrValidator(validate_ip_address, validate_hostname)
|
validate = OrValidator(validate_ip_address, validate_hostname)
|
||||||
validate_hostname(self.name)
|
validate_hostname(self.name)
|
||||||
try:
|
try:
|
||||||
|
@ -75,7 +76,7 @@ class BackendLog(models.Model):
|
||||||
NOTHING = 'NOTHING'
|
NOTHING = 'NOTHING'
|
||||||
# Special state for mocked backendlogs
|
# Special state for mocked backendlogs
|
||||||
EXCEPTION = 'EXCEPTION'
|
EXCEPTION = 'EXCEPTION'
|
||||||
|
|
||||||
STATES = (
|
STATES = (
|
||||||
(RECEIVED, RECEIVED),
|
(RECEIVED, RECEIVED),
|
||||||
(TIMEOUT, TIMEOUT),
|
(TIMEOUT, TIMEOUT),
|
||||||
|
@ -87,10 +88,10 @@ class BackendLog(models.Model):
|
||||||
(REVOKED, REVOKED),
|
(REVOKED, REVOKED),
|
||||||
(NOTHING, NOTHING),
|
(NOTHING, NOTHING),
|
||||||
)
|
)
|
||||||
|
|
||||||
backend = models.CharField(_("backend"), max_length=256)
|
backend = models.CharField(_("backend"), max_length=256)
|
||||||
state = models.CharField(_("state"), max_length=16, choices=STATES, default=RECEIVED)
|
state = models.CharField(_("state"), max_length=16, choices=STATES, default=RECEIVED)
|
||||||
server = models.ForeignKey(Server, verbose_name=_("server"), related_name='execution_logs')
|
server = models.ForeignKey(Server, verbose_name=_("server"), related_name='execution_logs', on_delete=models.CASCADE)
|
||||||
script = models.TextField(_("script"))
|
script = models.TextField(_("script"))
|
||||||
stdout = models.TextField(_("stdout"))
|
stdout = models.TextField(_("stdout"))
|
||||||
stderr = models.TextField(_("stderr"))
|
stderr = models.TextField(_("stderr"))
|
||||||
|
@ -100,25 +101,25 @@ class BackendLog(models.Model):
|
||||||
help_text="Celery task ID when used as execution backend")
|
help_text="Celery task ID when used as execution backend")
|
||||||
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
||||||
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'id'
|
get_latest_by = 'id'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s@%s" % (self.backend, self.server)
|
return "%s@%s" % (self.backend, self.server)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def execution_time(self):
|
def execution_time(self):
|
||||||
return (self.updated_at-self.created_at).total_seconds()
|
return (self.updated_at-self.created_at).total_seconds()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_finished(self):
|
def has_finished(self):
|
||||||
return self.state not in (self.STARTED, self.RECEIVED)
|
return self.state not in (self.STARTED, self.RECEIVED)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_success(self):
|
def is_success(self):
|
||||||
return self.state in (self.SUCCESS, self.NOTHING)
|
return self.state in (self.SUCCESS, self.NOTHING)
|
||||||
|
|
||||||
def backend_class(self):
|
def backend_class(self):
|
||||||
return ServiceBackend.get_backend(self.backend)
|
return ServiceBackend.get_backend(self.backend)
|
||||||
|
|
||||||
|
@ -135,26 +136,26 @@ class BackendOperation(models.Model):
|
||||||
"""
|
"""
|
||||||
Encapsulates an operation, storing its related object, the action and the backend.
|
Encapsulates an operation, storing its related object, the action and the backend.
|
||||||
"""
|
"""
|
||||||
log = models.ForeignKey('orchestration.BackendLog', related_name='operations')
|
log = models.ForeignKey('orchestration.BackendLog', related_name='operations', on_delete=models.CASCADE)
|
||||||
backend = models.CharField(_("backend"), max_length=256)
|
backend = models.CharField(_("backend"), max_length=256)
|
||||||
action = models.CharField(_("action"), max_length=64)
|
action = models.CharField(_("action"), max_length=64)
|
||||||
content_type = models.ForeignKey(ContentType)
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
object_id = models.PositiveIntegerField(null=True)
|
object_id = models.PositiveIntegerField(null=True)
|
||||||
instance_repr = models.CharField(_("instance representation"), max_length=256)
|
instance_repr = models.CharField(_("instance representation"), max_length=256)
|
||||||
|
|
||||||
instance = GenericForeignKey('content_type', 'object_id')
|
instance = GenericForeignKey('content_type', 'object_id')
|
||||||
objects = BackendOperationQuerySet.as_manager()
|
objects = BackendOperationQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Operation")
|
verbose_name = _("Operation")
|
||||||
verbose_name_plural = _("Operations")
|
verbose_name_plural = _("Operations")
|
||||||
index_together = (
|
index_together = (
|
||||||
('content_type', 'object_id'),
|
('content_type', 'object_id'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s.%s(%s)' % (self.backend, self.action, self.instance or self.instance_repr)
|
return '%s.%s(%s)' % (self.backend, self.action, self.instance or self.instance_repr)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def backend_class(self):
|
def backend_class(self):
|
||||||
return ServiceBackend.get_backend(self.backend)
|
return ServiceBackend.get_backend(self.backend)
|
||||||
|
@ -199,11 +200,11 @@ class Route(models.Model):
|
||||||
"""
|
"""
|
||||||
backend = models.CharField(_("backend"), max_length=256,
|
backend = models.CharField(_("backend"), max_length=256,
|
||||||
choices=ServiceBackend.get_choices())
|
choices=ServiceBackend.get_choices())
|
||||||
host = models.ForeignKey(Server, verbose_name=_("host"), related_name='routes')
|
host = models.ForeignKey(Server, verbose_name=_("host"), related_name='routes', on_delete=models.CASCADE)
|
||||||
match = models.CharField(_("match"), max_length=256, blank=True, default='True',
|
match = models.CharField(_("match"), max_length=256, blank=True, default='True',
|
||||||
help_text=_("Python expression used for selecting the targe host, "
|
help_text=_("Python expression used for selecting the targe host, "
|
||||||
"<em>instance</em> referes to the current object."))
|
"<em>instance</em> referes to the current object."))
|
||||||
async = models.BooleanField(default=False,
|
run_async = models.BooleanField(default=False,
|
||||||
help_text=_("Whether or not block the request/response cycle waitting this backend to "
|
help_text=_("Whether or not block the request/response cycle waitting this backend to "
|
||||||
"finish its execution. Usually you want slave servers to run asynchronously."))
|
"finish its execution. Usually you want slave servers to run asynchronously."))
|
||||||
async_actions = MultiSelectField(max_length=256, blank=True,
|
async_actions = MultiSelectField(max_length=256, blank=True,
|
||||||
|
@ -211,19 +212,19 @@ class Route(models.Model):
|
||||||
# method = models.CharField(_("method"), max_lenght=32, choices=method_choices,
|
# method = models.CharField(_("method"), max_lenght=32, choices=method_choices,
|
||||||
# default=MethodBackend.get_default())
|
# default=MethodBackend.get_default())
|
||||||
is_active = models.BooleanField(_("active"), default=True)
|
is_active = models.BooleanField(_("active"), default=True)
|
||||||
|
|
||||||
objects = RouteQuerySet.as_manager()
|
objects = RouteQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('backend', 'host')
|
unique_together = ('backend', 'host')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s@%s" % (self.backend, self.host)
|
return "%s@%s" % (self.backend, self.host)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def backend_class(self):
|
def backend_class(self):
|
||||||
return ServiceBackend.get_backend(self.backend)
|
return ServiceBackend.get_backend(self.backend)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if not self.match:
|
if not self.match:
|
||||||
self.match = 'True'
|
self.match = 'True'
|
||||||
|
@ -244,10 +245,10 @@ class Route(models.Model):
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
name = type(exception).__name__
|
name = type(exception).__name__
|
||||||
raise ValidationError(': '.join((name, str(exception))))
|
raise ValidationError(': '.join((name, str(exception))))
|
||||||
|
|
||||||
def action_is_async(self, action):
|
def action_is_async(self, action):
|
||||||
return action in self.async_actions
|
return action in self.async_actions
|
||||||
|
|
||||||
def matches(self, instance):
|
def matches(self, instance):
|
||||||
safe_locals = {
|
safe_locals = {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
|
@ -255,11 +256,11 @@ class Route(models.Model):
|
||||||
instance._meta.model_name: instance,
|
instance._meta.model_name: instance,
|
||||||
}
|
}
|
||||||
return eval(self.match, safe_locals)
|
return eval(self.match, safe_locals)
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.save()
|
self.save()
|
||||||
|
|
|
@ -12,7 +12,7 @@ class RouterTests(BaseTestCase):
|
||||||
|
|
||||||
def test_list_backends(self):
|
def test_list_backends(self):
|
||||||
# TODO count actual, register and compare
|
# TODO count actual, register and compare
|
||||||
choices = list(Route._meta.get_field('backend')._choices)
|
choices = list(Route._meta.get_field('backend').choices)
|
||||||
self.assertLess(1, len(choices))
|
self.assertLess(1, len(choices))
|
||||||
|
|
||||||
def test_get_instances(self):
|
def test_get_instances(self):
|
||||||
|
@ -25,7 +25,7 @@ class RouterTests(BaseTestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
choices = backends.ServiceBackend.get_choices()
|
choices = backends.ServiceBackend.get_choices()
|
||||||
Route._meta.get_field('backend')._choices = choices
|
Route._meta.get_field('backend').choices = choices
|
||||||
backend = TestBackend.get_name()
|
backend = TestBackend.get_name()
|
||||||
|
|
||||||
route = Route.objects.create(backend=backend, host=self.host, match='True')
|
route = Route.objects.create(backend=backend, host=self.host, match='True')
|
||||||
|
|
|
@ -6,20 +6,25 @@ def retrieve_state(servers):
|
||||||
pings = []
|
pings = []
|
||||||
for server in servers:
|
for server in servers:
|
||||||
address = server.get_address()
|
address = server.get_address()
|
||||||
ping = run('ping -c 1 -w 1 %s' % address, async=True)
|
ping = run('ping -c 1 -w 1 %s' % address, run_async=True)
|
||||||
pings.append(ping)
|
pings.append(ping)
|
||||||
uptime = sshrun(address, 'uptime', persist=True, async=True, options={'ConnectTimeout': 1})
|
uptime = sshrun(address, 'uptime', persist=True, run_async=True, options={'ConnectTimeout': 1})
|
||||||
uptimes.append(uptime)
|
uptimes.append(uptime)
|
||||||
|
|
||||||
state = {}
|
state = {}
|
||||||
for server, ping, uptime in zip(servers, pings, uptimes):
|
for server, ping, uptime in zip(servers, pings, uptimes):
|
||||||
ping = join(ping, silent=True)
|
ping = join(ping, silent=True)
|
||||||
ping = ping.stdout.splitlines()[-1].decode()
|
|
||||||
|
try:
|
||||||
|
ping = ping.stdout.splitlines()[-1].decode()
|
||||||
|
except IndexError:
|
||||||
|
ping = ''
|
||||||
|
|
||||||
if ping.startswith('rtt'):
|
if ping.startswith('rtt'):
|
||||||
ping = '%s ms' % ping.split('/')[4]
|
ping = '%s ms' % ping.split('/')[4]
|
||||||
else:
|
else:
|
||||||
ping = '<span style="color:red">Offline</span>'
|
ping = '<span style="color:red">Offline</span>'
|
||||||
|
|
||||||
uptime = join(uptime, silent=True)
|
uptime = join(uptime, silent=True)
|
||||||
uptime_stderr = uptime.stderr.decode()
|
uptime_stderr = uptime.stderr.decode()
|
||||||
uptime = uptime.stdout.decode().split()
|
uptime = uptime.stdout.decode().split()
|
||||||
|
@ -28,5 +33,5 @@ def retrieve_state(servers):
|
||||||
else:
|
else:
|
||||||
uptime = '<span style="color:red">%s</span>' % uptime_stderr
|
uptime = '<span style="color:red">%s</span>' % uptime_stderr
|
||||||
state[server.pk] = (ping, uptime)
|
state[server.pk] = (ping, uptime)
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -17,7 +17,7 @@ class BillSelectedOrders(object):
|
||||||
verbose_name = _("Bill")
|
verbose_name = _("Bill")
|
||||||
template = 'admin/orders/order/bill_selected_options.html'
|
template = 'admin/orders/order/bill_selected_options.html'
|
||||||
__name__ = 'bill_selected_orders'
|
__name__ = 'bill_selected_orders'
|
||||||
|
|
||||||
def __call__(self, modeladmin, request, queryset):
|
def __call__(self, modeladmin, request, queryset):
|
||||||
""" make this monster behave like a function """
|
""" make this monster behave like a function """
|
||||||
self.modeladmin = modeladmin
|
self.modeladmin = modeladmin
|
||||||
|
@ -34,7 +34,7 @@ class BillSelectedOrders(object):
|
||||||
del(self.queryset)
|
del(self.queryset)
|
||||||
del(self.context)
|
del(self.context)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def set_options(self, request):
|
def set_options(self, request):
|
||||||
form = BillSelectedOptionsForm()
|
form = BillSelectedOptionsForm()
|
||||||
if request.POST.get('step'):
|
if request.POST.get('step'):
|
||||||
|
@ -56,7 +56,7 @@ class BillSelectedOrders(object):
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
return render(request, self.template, self.context)
|
return render(request, self.template, self.context)
|
||||||
|
|
||||||
def select_related(self, request):
|
def select_related(self, request):
|
||||||
# TODO use changelist ?
|
# TODO use changelist ?
|
||||||
related = self.queryset.get_related().select_related('account', 'service')
|
related = self.queryset.get_related().select_related('account', 'service')
|
||||||
|
@ -76,7 +76,7 @@ class BillSelectedOrders(object):
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
return render(request, self.template, self.context)
|
return render(request, self.template, self.context)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def confirmation(self, request):
|
def confirmation(self, request):
|
||||||
form = BillSelectConfirmationForm(initial=self.options)
|
form = BillSelectConfirmationForm(initial=self.options)
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
|
from datetime import datetime
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_link, admin_date, change_url
|
from orchestra.admin.utils import admin_link, admin_date, change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
||||||
|
@ -22,10 +23,10 @@ class MetricStorageInline(admin.TabularInline):
|
||||||
model = MetricStorage
|
model = MetricStorage
|
||||||
readonly_fields = ('value', 'created_on', 'updated_on')
|
readonly_fields = ('value', 'created_on', 'updated_on')
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
def has_add_permission(self, request, obj=None):
|
def has_add_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
if obj:
|
if obj:
|
||||||
url = reverse('admin:orders_metricstorage_changelist')
|
url = reverse('admin:orders_metricstorage_changelist')
|
||||||
|
@ -33,7 +34,7 @@ class MetricStorageInline(admin.TabularInline):
|
||||||
title = _('Metric storage, last 10 entries, <a href="%s">(See all)</a>')
|
title = _('Metric storage, last 10 entries, <a href="%s">(See all)</a>')
|
||||||
self.verbose_name_plural = mark_safe(title % url)
|
self.verbose_name_plural = mark_safe(title % url)
|
||||||
return super(MetricStorageInline, self).get_fieldsets(request, obj)
|
return super(MetricStorageInline, self).get_fieldsets(request, obj)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(MetricStorageInline, self).get_queryset(request)
|
qs = super(MetricStorageInline, self).get_queryset(request)
|
||||||
change_view = bool(self.parent_object and self.parent_object.pk)
|
change_view = bool(self.parent_object and self.parent_object.pk)
|
||||||
|
@ -106,17 +107,16 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
'content_object_repr', 'content_object_link', 'bills_links', 'account_link',
|
'content_object_repr', 'content_object_link', 'bills_links', 'account_link',
|
||||||
'service_link'
|
'service_link'
|
||||||
)
|
)
|
||||||
|
|
||||||
service_link = admin_link('service')
|
service_link = admin_link('service')
|
||||||
display_registered_on = admin_date('registered_on')
|
display_registered_on = admin_date('registered_on')
|
||||||
display_cancelled_on = admin_date('cancelled_on')
|
display_cancelled_on = admin_date('cancelled_on')
|
||||||
|
|
||||||
def display_description(self, order):
|
def display_description(self, order):
|
||||||
return order.description[:64]
|
return format_html(order.description[:64])
|
||||||
display_description.short_description = _("Description")
|
display_description.short_description = _("Description")
|
||||||
display_description.allow_tags = True
|
|
||||||
display_description.admin_order_field = 'description'
|
display_description.admin_order_field = 'description'
|
||||||
|
|
||||||
def content_object_link(self, order):
|
def content_object_link(self, order):
|
||||||
if order.content_object:
|
if order.content_object:
|
||||||
try:
|
try:
|
||||||
|
@ -125,13 +125,13 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
# Does not has admin
|
# Does not has admin
|
||||||
return order.content_object_repr
|
return order.content_object_repr
|
||||||
description = str(order.content_object)
|
description = str(order.content_object)
|
||||||
return '<a href="{url}">{description}</a>'.format(
|
return format_html('<a href="{url}">{description}</a>',
|
||||||
url=url, description=description)
|
url=url, description=description)
|
||||||
return order.content_object_repr
|
return order.content_object_repr
|
||||||
content_object_link.short_description = _("Content object")
|
content_object_link.short_description = _("Content object")
|
||||||
content_object_link.allow_tags = True
|
|
||||||
content_object_link.admin_order_field = 'content_object_repr'
|
content_object_link.admin_order_field = 'content_object_repr'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def bills_links(self, order):
|
def bills_links(self, order):
|
||||||
bills = []
|
bills = []
|
||||||
make_link = admin_link()
|
make_link = admin_link()
|
||||||
|
@ -139,8 +139,7 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
bills.append(make_link(line.bill))
|
bills.append(make_link(line.bill))
|
||||||
return '<br>'.join(bills)
|
return '<br>'.join(bills)
|
||||||
bills_links.short_description = _("Bills")
|
bills_links.short_description = _("Bills")
|
||||||
bills_links.allow_tags = True
|
|
||||||
|
|
||||||
def display_billed_until(self, order):
|
def display_billed_until(self, order):
|
||||||
billed_until = order.billed_until
|
billed_until = order.billed_until
|
||||||
red = False
|
red = False
|
||||||
|
@ -156,14 +155,14 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
red = True
|
red = True
|
||||||
elif billed_until < timezone.now().date():
|
elif billed_until < timezone.now().date():
|
||||||
red = True
|
red = True
|
||||||
color = 'style="color:red;"' if red else ''
|
color = mark_safe('style="color:red;"') if red else ''
|
||||||
return '<span title="{raw}" {color}>{human}</span>'.format(
|
return format_html(
|
||||||
|
'<span title="{raw}" {color}>{human}</span>',
|
||||||
raw=escape(str(billed_until)), color=color, human=human,
|
raw=escape(str(billed_until)), color=color, human=human,
|
||||||
)
|
)
|
||||||
display_billed_until.short_description = _("billed until")
|
display_billed_until.short_description = _("billed until")
|
||||||
display_billed_until.allow_tags = True
|
|
||||||
display_billed_until.admin_order_field = 'billed_until'
|
display_billed_until.admin_order_field = 'billed_until'
|
||||||
|
|
||||||
def display_metric(self, order):
|
def display_metric(self, order):
|
||||||
"""
|
"""
|
||||||
dispalys latest metric value, don't uses latest() because not loosing prefetch_related
|
dispalys latest metric value, don't uses latest() because not loosing prefetch_related
|
||||||
|
@ -174,7 +173,7 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
return ''
|
return ''
|
||||||
return metric.value
|
return metric.value
|
||||||
display_metric.short_description = _("Metric")
|
display_metric.short_description = _("Metric")
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'description':
|
if db_field.name == 'description':
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue