Compare commits
98 Commits
dev/github
...
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 |
|
@ -1,5 +0,0 @@
|
|||
SECRET_KEY=k_=*vfue(^campsl63)7w5m&cu9u4o4-!vaw94qzyrymyv0hgg
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=.localhost,127.0.0.1
|
||||
DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME
|
||||
STATIC_ROOT=PATH_TO_STATIC_ROOT
|
|
@ -1,76 +0,0 @@
|
|||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Service containers to run with `container-job`
|
||||
services:
|
||||
# Label used to access the service container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_DB: test_myapp
|
||||
POSTGRES_USER: testuser
|
||||
POSTGRES_PASSWORD: s3cretPass
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.6]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -qy
|
||||
sudo apt-get -y install python3-dev libxml2 libxml2-dev libxslt-dev bind9utils ca-certificates gettext libcrack2-dev libxml2-dev libxslt1-dev ssh-client wget xvfb zlib1g-dev git iceweasel dnsutils postgresql-contrib libgirepository1.0-dev
|
||||
python -m pip install --upgrade pip
|
||||
pip install wheel
|
||||
pip install -r total_requirements.txt
|
||||
pip install -e .
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings.
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics
|
||||
- name: Run Tests
|
||||
run: |
|
||||
# orchestra-admin startproject panel
|
||||
django-admin.py startproject panel --template=orchestra/conf/ribaguifi_template -v3
|
||||
#python panel/manage.py test orchestra --noinput -v3
|
||||
coverage run --source='orchestra' panel/manage.py test orchestra --noinput -v3
|
||||
coverage report
|
||||
coverage xml
|
||||
|
||||
env:
|
||||
SECRET_KEY: zrhnooq6)sb+0+xb)(o0rvbf5)a(vc8ncv&1&kng@3i_pmx3oy
|
||||
DEBUG: True
|
||||
ALLOWED_HOSTS: .localhost,127.0.0.1
|
||||
DATABASE_URL: postgres://testuser:s3cretPass@localhost:5432/test_myapp
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@ -1,70 +0,0 @@
|
|||
We need have python3.6
|
||||
|
||||
#Install Packages
|
||||
```bash
|
||||
apt=(
|
||||
bind9utils
|
||||
ca-certificates
|
||||
gettext
|
||||
libcrack2-dev
|
||||
libxml2-dev
|
||||
libxslt1-dev
|
||||
ssh-client
|
||||
wget
|
||||
xvfb
|
||||
zlib1g-dev
|
||||
git
|
||||
iceweasel
|
||||
dnsutils
|
||||
postgresql-contrib
|
||||
)
|
||||
sudo apt-get install --no-install-recommends -y ${apt[@]}
|
||||
```
|
||||
|
||||
It is necessary install *wkhtmltopdf*
|
||||
You can install it from https://wkhtmltopdf.org/downloads.html
|
||||
|
||||
Clone this repository
|
||||
```bash
|
||||
git clone https://github.com/ribaguifi/django-orchestra
|
||||
```
|
||||
|
||||
Prepare env and install requirements
|
||||
```bash
|
||||
cd django-orchestra
|
||||
python3.6 -m venv env
|
||||
source env/bin/activate
|
||||
pip3 install --upgrade pip
|
||||
pip3 install -r total_requirements.txt
|
||||
pip3 install -e .
|
||||
```
|
||||
|
||||
Configure project using environment file (you can use provided example as quickstart):
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Prepare your Postgres database (create database, user and grant permissions):
|
||||
```sql
|
||||
CREATE DATABASE myproject;
|
||||
CREATE USER myuser WITH PASSWORD 'password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE myproject TO myuser;
|
||||
```
|
||||
|
||||
Prepare a new project:
|
||||
|
||||
```bash
|
||||
django-admin.py startproject PROJECT_NAME --template="orchestra/conf/ribaguifi_template"
|
||||
```
|
||||
|
||||
Run migrations:
|
||||
```bash
|
||||
python3 manage.py migrate
|
||||
```
|
||||
|
||||
(Optional) You can start a Django development server to check that everything is ok.
|
||||
```bash
|
||||
python3 manage.py runserver
|
||||
```
|
||||
|
||||
Open [http://127.0.0.1:8000/](http://127.0.0.1:8000/) in your browser.
|
|
@ -1,41 +0,0 @@
|
|||
FROM python:3.6
|
||||
|
||||
RUN apt-get -y update
|
||||
RUN pip3 install wheel
|
||||
|
||||
RUN apt-get -y install python3-dev
|
||||
|
||||
RUN apt-get install -y bind9utils ca-certificates gettext libcrack2-dev libxml2-dev\
|
||||
libxslt1-dev ssh-client wget xvfb zlib1g-dev git iceweasel dnsutils postgresql-contrib\
|
||||
curl sudo vim libgirepository1.0-dev
|
||||
|
||||
RUN apt-get clean
|
||||
|
||||
RUN useradd orchestra --shell /bin/bash && \
|
||||
{ echo "orchestra:orchestra" | chpasswd; } && \
|
||||
mkhomedir_helper orchestra && \
|
||||
adduser orchestra sudo
|
||||
|
||||
# RUN echo 'EXPORT $PATH="$PATH:/home/orchestra/.local/bin/"' > /home/orchestra/.bashrc
|
||||
# RUN git clone https://github.com/ribaguifi/django-orchestra.git
|
||||
# RUN orchestra-admin startproject panel
|
||||
# RUN python3 panel/manage.py migrate
|
||||
# RUN python3 panel/manage.py runserver
|
||||
|
||||
# install wkhtmltox
|
||||
RUN apt-get install -y xfonts-75dpi
|
||||
RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb -O /tmp/wkhtmltox.deb
|
||||
RUN dpkg -i /tmp/wkhtmltox.deb
|
||||
|
||||
RUN wget https://github.com/mozilla/geckodriver/releases/download/v0.29.0/geckodriver-v0.29.0-linux64.tar.gz -O /tmp/geckodriver.tar.gz
|
||||
RUN tar -xf /tmp/geckodriver.tar.gz -C /usr/local/bin/
|
||||
|
||||
# install orchestra requirements
|
||||
RUN pip3 install --upgrade pip
|
||||
|
||||
# TODO(@slamora): requirements.txt duplicates ../totaL_requirements.txt
|
||||
# Docker compose security policy forbiddes access to parent folders
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
EXPOSE 8000
|
|
@ -1,33 +0,0 @@
|
|||
# orchestra environment based on docker-compose
|
||||
|
||||
Docker compose environment to develop django-orchestra.
|
||||
|
||||
**NOTE**: On web container, volume `/code` contains the source code of the host.
|
||||
|
||||
1. Build (or rebuild if any change done) the containers:
|
||||
```
|
||||
cd examples/
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
2. Start the containers:
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
3. Run a bash on `web` container:
|
||||
```
|
||||
docker-compose run web bash
|
||||
```
|
||||
|
||||
4. Run on the web docker container the first time:
|
||||
```
|
||||
su - orchestra
|
||||
bash /code/examples/init_project.sh
|
||||
```
|
||||
|
||||
5. Run tests or do whatever you need:
|
||||
```
|
||||
cd panel
|
||||
python manage.py test --noinput orchestra.contrib.lists.tests.functional_tests.tests.AdminListTest.test_add
|
||||
```
|
|
@ -1,4 +0,0 @@
|
|||
create database orchestra;
|
||||
CREATE USER orchestra WITH PASSWORD 'orchestra';
|
||||
GRANT ALL PRIVILEGES ON DATABASE orchestra TO orchestra;
|
||||
ALTER ROLE orchestra CREATEDB;
|
|
@ -1,17 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- ..:/code
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_DB: orchestra
|
||||
POSTGRES_USER: orchestra
|
||||
POSTGRES_PASSWORD: orchestra
|
|
@ -1,5 +0,0 @@
|
|||
SECRET_KEY=zrhnooq6)sb+0+xb)(o0rvbf5)a(vc8ncv&1&kng@3i_pmx3oy
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=.localhost,127.0.0.1
|
||||
DATABASE_URL=postgres://orchestra:orchestra@postgres:5432/orchestra
|
||||
STATIC_ROOT=PATH_TO_STATIC_ROOT
|
|
@ -1,27 +0,0 @@
|
|||
sudo pip3 install -e /code
|
||||
psql -U orchestra -h postgres < /code/examples/createdb.sql
|
||||
|
||||
cd ~
|
||||
django-admin.py startproject panel --template="/code/orchestra/conf/ribaguifi_template"
|
||||
cp /code/examples/env.example panel/.env
|
||||
|
||||
cd panel
|
||||
python3 manage.py setupcronbeat
|
||||
python3 manage.py syncperiodictasks
|
||||
|
||||
sudo apt-get install -y rabbitmq-server
|
||||
sudo python3 manage.py setupcelery --username orchestra
|
||||
|
||||
sudo python3 manage.py setuplog
|
||||
|
||||
python3 manage.py collectstatic --noinput
|
||||
sudo apt-get install -y nginx-full uwsgi uwsgi-plugin-python3
|
||||
sudo python3 manage.py setupnginx --user orchestra
|
||||
|
||||
sudo /etc/init.d/rabbitmq-server start
|
||||
sudo pip uninstall django-celery
|
||||
sudo pip install -r /code/requirements.txt
|
||||
sudo python3 manage.py startservices
|
||||
|
||||
|
||||
# python3 panel/manage.py migrate
|
|
@ -1,53 +0,0 @@
|
|||
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
|
||||
djangorestframework==3.4.7
|
||||
django-celery-email
|
||||
django-debug-toolbar
|
||||
django-cors-headers
|
||||
django-countries
|
||||
django-filter==0.15.2
|
||||
django-flat-theme
|
||||
django-fluent-dashboard
|
||||
django-iban
|
||||
django-localflavor
|
||||
django-multiselectfield
|
||||
django-nose==1.4.4
|
||||
django-reversion
|
||||
django-transaction-signals
|
||||
celery==3.1.23
|
||||
kombu==3.0.35
|
||||
billiard==3.3.0.23
|
||||
Markdown==2.4
|
||||
ecdsa==0.11
|
||||
Pygments==1.6
|
||||
jsonfield==0.9.22
|
||||
python_dateutil
|
||||
requests
|
||||
phonenumbers
|
||||
amqp==1.4.9
|
||||
anyjson
|
||||
pytz
|
||||
cracklib
|
||||
lxml==3.3.5
|
||||
selenium
|
||||
xvfbwrapper
|
||||
freezegun==1.1.0
|
||||
coverage
|
||||
flake8
|
||||
sqlparse
|
||||
pyinotify
|
||||
PyMySQL
|
||||
dj_database_url==0.5.0
|
||||
psycopg2
|
||||
python-decouple
|
||||
https://github.com/glic3rinu/passlib/archive/master.zip
|
||||
paramiko
|
||||
mysqlclient
|
||||
pycrypto==2.6.1
|
||||
pygobject
|
||||
six
|
||||
nose
|
||||
-e git+https://github.com/ribaguifi/orchestra-orm.git#egg=orchestra-orm
|
|
@ -93,7 +93,7 @@ Remember create a database for your project and give permitions for the correct
|
|||
```
|
||||
psql -U postgres
|
||||
psql (12.4)
|
||||
Digite «help».
|
||||
Digite «help» para obtener ayuda.
|
||||
|
||||
postgres=# CREATE database orchesta;
|
||||
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';
|
||||
|
|
|
@ -3,7 +3,7 @@ from collections import OrderedDict
|
|||
from functools import update_wrapper
|
||||
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from fluent_dashboard import dashboard, appsettings
|
||||
from fluent_dashboard.modules import CmsAppIconList
|
||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
|||
from django.contrib.admin import helpers
|
||||
from django.core import validators
|
||||
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 orchestra.forms.widgets import SpanWidget
|
||||
|
@ -28,9 +28,9 @@ class AdminFormMixin(object):
|
|||
' {% include "admin/includes/fieldset.html" %}'
|
||||
'{% endfor %}'
|
||||
)
|
||||
context = Context({
|
||||
context = {
|
||||
'adminform': adminform
|
||||
})
|
||||
}
|
||||
return template.render(context)
|
||||
|
||||
|
||||
|
@ -71,9 +71,9 @@ class AdminFormSet(BaseModelFormSet):
|
|||
</div>
|
||||
</div>""")
|
||||
)
|
||||
context = Context({
|
||||
context = {
|
||||
'formset': self
|
||||
})
|
||||
}
|
||||
return template.render(context)
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from copy import deepcopy
|
||||
|
||||
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.translation import ugettext_lazy as _
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ from functools import wraps
|
|||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
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.shortcuts import redirect
|
||||
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 orchestra.models.utils import get_field_value
|
||||
|
@ -113,21 +113,21 @@ def admin_link(*args, **kwargs):
|
|||
return '---'
|
||||
if not getattr(obj, 'pk', None):
|
||||
return '---'
|
||||
display = kwargs.get('display')
|
||||
if display:
|
||||
display = getattr(obj, display, display)
|
||||
display_ = kwargs.get('display')
|
||||
if display_:
|
||||
display_ = getattr(obj, display_, display_)
|
||||
else:
|
||||
display = obj
|
||||
display_ = obj
|
||||
try:
|
||||
url = change_url(obj)
|
||||
except NoReverseMatch:
|
||||
# Does not has admin
|
||||
return str(display)
|
||||
return str(display_)
|
||||
extra = ''
|
||||
if kwargs['popup']:
|
||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
||||
extra = mark_safe('onclick="return showAddAnotherPopup(this);"')
|
||||
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
|
||||
|
@ -158,7 +158,7 @@ def admin_date(*args, **kwargs):
|
|||
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
else:
|
||||
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):
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
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 .serializers import SetPasswordSerializer
|
||||
|
||||
|
||||
class SetPasswordApiMixin(object):
|
||||
@detail_route(methods=['post'], serializer_class=SetPasswordSerializer)
|
||||
@action(detail=True, methods=['post'], serializer_class=SetPasswordSerializer)
|
||||
def set_password(self, request, pk):
|
||||
obj = self.get_object()
|
||||
data = request.data
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.core.urlresolvers import NoReverseMatch
|
||||
from django.urls import NoReverseMatch
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
|
@ -23,16 +23,16 @@ def link_wrap(view, view_names):
|
|||
return wrapper
|
||||
|
||||
|
||||
def insert_links(viewset, base_name):
|
||||
collection_links = ['api-root', '%s-list' % base_name]
|
||||
object_links = ['api-root', '%s-list' % base_name, '%s-detail' % base_name]
|
||||
def insert_links(viewset, basename):
|
||||
collection_links = ['api-root', '%s-list' % basename]
|
||||
object_links = ['api-root', '%s-list' % basename, '%s-detail' % basename]
|
||||
exception_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
|
||||
for methodname in dir(viewset):
|
||||
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'):
|
||||
list_links.append(view_name)
|
||||
retrieve_links.append(view_name)
|
||||
|
|
|
@ -65,12 +65,12 @@ class LinkHeaderRouter(DefaultRouter):
|
|||
APIRoot.router = self
|
||||
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 """
|
||||
if base_name is None:
|
||||
base_name = self.get_default_base_name(viewset)
|
||||
insert_links(viewset, base_name)
|
||||
self.registry.append((prefix, viewset, base_name))
|
||||
if basename is None:
|
||||
basename = self.get_default_basename(viewset)
|
||||
insert_links(viewset, basename)
|
||||
self.registry.append((prefix, viewset, basename))
|
||||
|
||||
def get_viewset(self, prefix_or_model):
|
||||
for _prefix, viewset, __ in self.registry:
|
||||
|
|
|
@ -23,7 +23,7 @@ class APIRoot(views.APIView):
|
|||
'accountancy': {},
|
||||
'services': {},
|
||||
}
|
||||
if not request.user.is_anonymous():
|
||||
if not request.user.is_anonymous:
|
||||
list_name = '{basename}-list'
|
||||
detail_name = '{basename}-detail'
|
||||
for prefix, viewset, basename in self.router.registry:
|
||||
|
|
|
@ -64,7 +64,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
|||
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
||||
""" returns object on to_internal_value based on URL """
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
url = data.get('url')
|
||||
except AttributeError:
|
||||
url = None
|
||||
if not url:
|
||||
raise ValidationError({
|
||||
'url': "URL is required."
|
||||
|
@ -81,14 +84,14 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
|||
validators=[validate_password], write_only=True, required=False,
|
||||
style={'widget': widgets.PasswordInput})
|
||||
|
||||
def validate_password(self, attrs, source):
|
||||
def validate_password(self, value):
|
||||
""" POST only password """
|
||||
if self.instance:
|
||||
if 'password' in attrs:
|
||||
if value:
|
||||
raise serializers.ValidationError(_("Can not set password"))
|
||||
elif 'password' not in attrs:
|
||||
elif not value:
|
||||
raise serializers.ValidationError(_("Password required"))
|
||||
return attrs
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
""" remove password in case is not a real model field """
|
||||
|
|
|
@ -116,17 +116,20 @@ function install_requirements () {
|
|||
update-locale LANG=en_US.UTF-8
|
||||
fi
|
||||
|
||||
# lxml: libxml2-dev, libxslt1-dev, zlib1g-dev
|
||||
APT="bind9utils \
|
||||
ca-certificates \
|
||||
gettext \
|
||||
libcrack2-dev \
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-dev \
|
||||
python3-lxml \
|
||||
ssh-client \
|
||||
wget \
|
||||
xvfb"
|
||||
xvfb \
|
||||
zlib1g-dev"
|
||||
if $testing; then
|
||||
APT="${APT} \
|
||||
git \
|
||||
|
@ -147,13 +150,14 @@ function install_requirements () {
|
|||
fi
|
||||
|
||||
# cracklib and lxml are excluded on the requirements.txt because they need unconvinient system dependencies
|
||||
PIP="$(wget https://raw.githubusercontent.com/ribaguifi/django-orchestra/dev/github-actions/requirements.txt -O - | tr '\n' ' ') \
|
||||
cracklib"
|
||||
PIP="$(wget http://git.io/orchestra-requirements.txt -O - | tr '\n' ' ') \
|
||||
cracklib \
|
||||
lxml==3.3.5"
|
||||
if $testing; then
|
||||
PIP="${PIP} \
|
||||
selenium \
|
||||
xvfbwrapper \
|
||||
freezegun \
|
||||
freezegun==0.3.14 \
|
||||
coverage \
|
||||
flake8 \
|
||||
django-debug-toolbar==1.3.0 \
|
||||
|
|
|
@ -178,7 +178,7 @@ def fire_pending_tasks(manage, db):
|
|||
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(
|
||||
manage=manage, task_id=task_id)
|
||||
proc = run(command, async=True)
|
||||
proc = run(command, run_async=True)
|
||||
yield proc
|
||||
|
||||
|
||||
|
@ -201,7 +201,7 @@ def fire_pending_messages(settings, db):
|
|||
|
||||
if has_pending_messages(settings, db):
|
||||
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
||||
proc = run(command, async=True)
|
||||
proc = run(command, run_async=True)
|
||||
yield proc
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ SECRET_KEY = '{{ secret_key }}'
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
|
||||
|
@ -65,6 +66,7 @@ INSTALLED_APPS = [
|
|||
'admin_tools.dashboard',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'django_filters',
|
||||
'passlib.ext.django',
|
||||
'django_countries',
|
||||
# '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'
|
||||
|
||||
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
|
||||
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
||||
|
||||
|
@ -168,22 +203,6 @@ LOCALE_PATHS = (
|
|||
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'
|
||||
|
||||
|
||||
|
@ -242,7 +261,6 @@ PASSLIB_CONFIG = (
|
|||
"default = sha512_crypt\n"
|
||||
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
||||
" django_des_crypt, des_crypt, hex_md5\n"
|
||||
"all__vary_rounds = 0.05\n"
|
||||
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
||||
"sha512_crypt__min_rounds = 80000\n"
|
||||
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.version_info < (3, 3):
|
||||
cmd = ' '.join(sys.argv)
|
||||
sys.stderr.write("Sorry, Orchestra requires at least Python 3.3, try with:\n$ python3 %s\n" % cmd)
|
||||
sys.exit(1)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings")
|
||||
from django.core.management import execute_from_command_line
|
||||
execute_from_command_line(sys.argv)
|
|
@ -1,261 +0,0 @@
|
|||
"""
|
||||
Django settings for {{ project_name }} project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django {{ django_version }}.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from decouple import config, Csv
|
||||
from dj_database_url import parse as db_url
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '{{ secret_key }}'
|
||||
# SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# django-orchestra apps
|
||||
'orchestra',
|
||||
'orchestra.contrib.accounts',
|
||||
'orchestra.contrib.systemusers',
|
||||
'orchestra.contrib.contacts',
|
||||
'orchestra.contrib.orchestration',
|
||||
'orchestra.contrib.bills',
|
||||
'orchestra.contrib.payments',
|
||||
'orchestra.contrib.tasks',
|
||||
'orchestra.contrib.mailer',
|
||||
'orchestra.contrib.history',
|
||||
'orchestra.contrib.issues',
|
||||
'orchestra.contrib.services',
|
||||
'orchestra.contrib.plans',
|
||||
'orchestra.contrib.orders',
|
||||
'orchestra.contrib.domains',
|
||||
'orchestra.contrib.mailboxes',
|
||||
'orchestra.contrib.lists',
|
||||
'orchestra.contrib.webapps',
|
||||
'orchestra.contrib.websites',
|
||||
'orchestra.contrib.letsencrypt',
|
||||
'orchestra.contrib.databases',
|
||||
'orchestra.contrib.vps',
|
||||
'orchestra.contrib.saas',
|
||||
'orchestra.contrib.miscellaneous',
|
||||
|
||||
# Third-party apps
|
||||
'django_extensions',
|
||||
'djcelery',
|
||||
'fluent_dashboard',
|
||||
'admin_tools',
|
||||
'admin_tools.theming',
|
||||
'admin_tools.menu',
|
||||
'admin_tools.dashboard',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'passlib.ext.django',
|
||||
'django_countries',
|
||||
# 'debug_toolbar',
|
||||
|
||||
# Django.contrib
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.admin.apps.SimpleAdminConfig',
|
||||
|
||||
# Last to load
|
||||
'orchestra.contrib.resources',
|
||||
'orchestra.contrib.settings',
|
||||
# 'django_nose',
|
||||
]
|
||||
|
||||
|
||||
ROOT_URLCONF = '{{ project_name }}.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'orchestra.core.context_processors.site',
|
||||
],
|
||||
'loaders': [
|
||||
'admin_tools.template_loaders.Loader',
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
WSGI_APPLICATION = '{{ project_name }}.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': config(
|
||||
'DATABASE_URL',
|
||||
default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
cast=db_url
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
|
||||
try:
|
||||
TIME_ZONE = open('/etc/timezone', 'r').read().strip()
|
||||
except IOError:
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
|
||||
# Path used for database translations files
|
||||
LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
)
|
||||
|
||||
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'
|
||||
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'orchestra.permissions.auth.OrchestraPermissionBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
]
|
||||
|
||||
|
||||
EMAIL_BACKEND = 'orchestra.contrib.mailer.backends.EmailBackend'
|
||||
|
||||
|
||||
# Needed for Bulk operations
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
|
||||
|
||||
|
||||
#################################
|
||||
## 3RD PARTY APPS CONIGURATION ##
|
||||
#################################
|
||||
|
||||
# Admin Tools
|
||||
ADMIN_TOOLS_MENU = 'orchestra.admin.menu.OrchestraMenu'
|
||||
|
||||
# Fluent dashboard
|
||||
ADMIN_TOOLS_INDEX_DASHBOARD = 'orchestra.admin.dashboard.OrchestraIndexDashboard'
|
||||
FLUENT_DASHBOARD_ICON_THEME = '../orchestra/icons'
|
||||
|
||||
|
||||
# Django-celery
|
||||
import djcelery
|
||||
djcelery.setup_loader()
|
||||
CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'
|
||||
CELERY_ALWAYS_EAGER = True
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
task_always_eager = True
|
||||
TASK_ALWAYS_EAGER = True
|
||||
|
||||
|
||||
# rest_framework
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'orchestra.permissions.api.OrchestraPermissionBackend',
|
||||
),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
('rest_framework.filters.DjangoFilterBackend',)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# Use a UNIX compatible hash
|
||||
PASSLIB_CONFIG = (
|
||||
"[passlib]\n"
|
||||
"schemes = sha512_crypt, django_pbkdf2_sha256, django_pbkdf2_sha1, "
|
||||
" django_bcrypt, django_bcrypt_sha256, django_salted_sha1, des_crypt, "
|
||||
" django_salted_md5, django_des_crypt, hex_md5, bcrypt, phpass\n"
|
||||
"default = sha512_crypt\n"
|
||||
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
||||
" django_des_crypt, des_crypt, hex_md5\n"
|
||||
"all__vary_rounds = 0.05\n"
|
||||
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
||||
"sha512_crypt__min_rounds = 80000\n"
|
||||
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
||||
"staff__sha512_crypt__default_rounds = 100000\n"
|
||||
"superuser__django_pbkdf2_sha256__default_rounds = 15000\n"
|
||||
"superuser__sha512_crypt__default_rounds = 120000\n"
|
||||
)
|
||||
|
||||
|
||||
SHELL_PLUS_PRE_IMPORTS = (
|
||||
('orchestra.contrib.orchestration.managers', ('orchestrate',)),
|
||||
)
|
|
@ -1,6 +0,0 @@
|
|||
from django.conf.urls import include, url
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'', include('orchestra.urls')),
|
||||
]
|
|
@ -1,14 +0,0 @@
|
|||
"""
|
||||
WSGI config for {{ project_name }} project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings")
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
|
@ -4,7 +4,7 @@ from django.contrib import messages
|
|||
from django.contrib.admin import helpers
|
||||
from django.contrib.admin.utils import NestedObjects, quote
|
||||
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.shortcuts import redirect, render
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -175,7 +175,7 @@ def delete_related_services(modeladmin, request, queryset):
|
|||
for model, objs in collector.model_objs.items():
|
||||
count = 0
|
||||
# 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
|
||||
# Discount account
|
||||
elif model is not modeladmin.model and model in registered_services:
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.conf.urls import url
|
|||
from django.contrib import admin, messages
|
||||
from django.contrib.admin.utils import unquote
|
||||
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.templatetags.static import static
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -158,6 +158,7 @@ class AccountListAdmin(AccountAdmin):
|
|||
actions = None
|
||||
change_list_template = 'admin/accounts/account/select_account_list.html'
|
||||
|
||||
@mark_safe
|
||||
def select_account(self, instance):
|
||||
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
||||
context = {
|
||||
|
@ -167,7 +168,6 @@ class AccountListAdmin(AccountAdmin):
|
|||
}
|
||||
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
||||
select_account.short_description = _("account")
|
||||
select_account.allow_tags = True
|
||||
select_account.admin_order_field = 'username'
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
|
@ -207,6 +207,7 @@ class AccountAdminMixin(object):
|
|||
account = None
|
||||
list_select_related = ('account',)
|
||||
|
||||
@mark_safe
|
||||
def display_active(self, instance):
|
||||
if not instance.is_active:
|
||||
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
||||
|
@ -215,14 +216,12 @@ class AccountAdminMixin(object):
|
|||
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')
|
||||
display_active.short_description = _("active")
|
||||
display_active.allow_tags = True
|
||||
display_active.admin_order_field = 'is_active'
|
||||
|
||||
def account_link(self, instance):
|
||||
account = instance.account if instance.pk else self.account
|
||||
return admin_link()(account)
|
||||
account_link.short_description = _("account")
|
||||
account_link.allow_tags = True
|
||||
account_link.admin_order_field = 'account__username'
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
|
|
|
@ -47,7 +47,7 @@ def create_account_creation_form():
|
|||
# Previous validation error
|
||||
return
|
||||
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():
|
||||
errors['username'] = _("A system user with this name already exists.")
|
||||
for model, key, related_kwargs, __ in create_related:
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
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_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)),
|
||||
('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={
|
||||
'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')
|
||||
])
|
||||
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)
|
||||
full_name = models.CharField(_("full name"), max_length=256)
|
||||
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
||||
|
@ -53,6 +53,7 @@ class Account(auth.AbstractBaseUser):
|
|||
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)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from datetime import date
|
|||
from django.contrib import messages
|
||||
from django.contrib.admin import helpers
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db import transaction
|
||||
from django.forms.models import modelformset_factory
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
|
|
|
@ -2,11 +2,12 @@ from django import forms
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin, messages
|
||||
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.models import F, Sum, Prefetch
|
||||
from django.db.models.functions import Coalesce
|
||||
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_lazy as _
|
||||
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.contrib.accounts.actions import list_accounts
|
||||
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 .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
||||
|
@ -67,6 +68,7 @@ class BillLineInline(admin.TabularInline):
|
|||
|
||||
order_link = admin_link('order', display='pk')
|
||||
|
||||
@mark_safe
|
||||
def display_total(self, line):
|
||||
if line.pk:
|
||||
total = line.compute_total()
|
||||
|
@ -78,7 +80,6 @@ class BillLineInline(admin.TabularInline):
|
|||
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
|
||||
return '<a href="%s">%s</a>' % (url, total)
|
||||
display_total.short_description = _("Total")
|
||||
display_total.allow_tags = True
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Make value input widget bigger """
|
||||
|
@ -104,27 +105,26 @@ class ClosedBillLineInline(BillLineInline):
|
|||
readonly_fields = fields
|
||||
can_delete = False
|
||||
|
||||
@mark_safe
|
||||
def display_description(self, line):
|
||||
descriptions = [line.description]
|
||||
for subline in line.sublines.all():
|
||||
descriptions.append(' ' * 4 + subline.description)
|
||||
return '<br>'.join(descriptions)
|
||||
display_description.short_description = _("Description")
|
||||
display_description.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def display_subtotal(self, line):
|
||||
subtotals = [' ' + str(line.subtotal)]
|
||||
for subline in line.sublines.all():
|
||||
subtotals.append(str(subline.total))
|
||||
return '<br>'.join(subtotals)
|
||||
display_subtotal.short_description = _("Subtotal")
|
||||
display_subtotal.allow_tags = True
|
||||
|
||||
def display_total(self, line):
|
||||
if line.pk:
|
||||
return line.compute_total()
|
||||
display_total.short_description = _("Total")
|
||||
display_total.allow_tags = True
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
@ -242,6 +242,7 @@ class BillLineManagerAdmin(BillLineAdmin):
|
|||
|
||||
|
||||
class BillAdminMixin(AccountAdminMixin):
|
||||
@mark_safe
|
||||
def display_total_with_subtotals(self, bill):
|
||||
if bill.pk:
|
||||
currency = settings.BILLS_CURRENCY.lower()
|
||||
|
@ -251,10 +252,10 @@ class BillAdminMixin(AccountAdminMixin):
|
|||
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
||||
subtotals = '\n'.join(subtotals)
|
||||
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.admin_order_field = 'approx_total'
|
||||
|
||||
@mark_safe
|
||||
def display_payment_state(self, bill):
|
||||
if bill.pk:
|
||||
t_opts = bill.transactions.model._meta
|
||||
|
@ -276,7 +277,6 @@ class BillAdminMixin(AccountAdminMixin):
|
|||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
||||
url=url, color=color, name=state, title=title)
|
||||
display_payment_state.allow_tags = True
|
||||
display_payment_state.short_description = _("Payment")
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
@ -376,16 +376,14 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
|||
|
||||
def display_total(self, bill):
|
||||
currency = settings.BILLS_CURRENCY.lower()
|
||||
return '%s &%s;' % (bill.compute_total(), currency)
|
||||
display_total.allow_tags = True
|
||||
return format_html('{} &{};', bill.compute_total(), currency)
|
||||
display_total.short_description = _("total")
|
||||
display_total.admin_order_field = 'approx_total'
|
||||
|
||||
def type_link(self, bill):
|
||||
bill_type = bill.type.lower()
|
||||
url = reverse('admin:bills_%s_changelist' % bill_type)
|
||||
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
|
||||
type_link.allow_tags = True
|
||||
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
|
||||
type_link.short_description = _("type")
|
||||
type_link.admin_order_field = 'type'
|
||||
|
||||
|
@ -479,7 +477,7 @@ class BillContactInline(admin.StackedInline):
|
|||
if db_field.name == 'address':
|
||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||
if db_field.name == 'email_usage':
|
||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
||||
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.http import HttpResponse
|
||||
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.contrib.accounts.api import AccountApiMixin
|
||||
|
@ -15,7 +15,7 @@ class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
|||
queryset = Bill.objects.all()
|
||||
serializer_class = BillSerializer
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
@action(detail=True, methods=['get'])
|
||||
def document(self, request, pk):
|
||||
bill = self.get_object()
|
||||
content_type = request.META.get('HTTP_ACCEPT')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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.html import format_html
|
||||
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)
|
||||
send(request, mark_safe(message))
|
||||
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'):
|
||||
account = force_text(main)
|
||||
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 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
@ -14,7 +15,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='bill',
|
||||
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(
|
||||
model_name='billcontact',
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,12 @@
|
|||
import datetime
|
||||
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.db import models
|
||||
from django.db.models import F, Sum
|
||||
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.encoding import force_text
|
||||
from django.utils.functional import cached_property
|
||||
|
@ -24,7 +24,7 @@ from . import settings
|
|||
|
||||
class BillContact(models.Model):
|
||||
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,
|
||||
help_text=_("Account full name will be used when left blank."))
|
||||
address = models.TextField(_("address"))
|
||||
|
@ -102,9 +102,9 @@ class Bill(models.Model):
|
|||
|
||||
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
||||
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"),
|
||||
related_name='amends')
|
||||
related_name='amends', on_delete=models.SET_NULL)
|
||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
||||
|
@ -303,7 +303,7 @@ class Bill(models.Model):
|
|||
with translation.override(language or self.account.language):
|
||||
if payment is False:
|
||||
payment = self.account.paymentsources.get_default()
|
||||
context = Context({
|
||||
context = {
|
||||
'bill': self,
|
||||
'lines': self.lines.all().prefetch_related('sublines'),
|
||||
'seller': self.seller,
|
||||
|
@ -318,7 +318,7 @@ class Bill(models.Model):
|
|||
'payment': payment and payment.get_bill_context(),
|
||||
'default_due_date': self.get_due_date(payment=payment),
|
||||
'now': timezone.now(),
|
||||
})
|
||||
}
|
||||
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
||||
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
||||
bill_template = loader.get_template(template)
|
||||
|
@ -416,7 +416,7 @@ class ProForma(Bill):
|
|||
|
||||
class BillLine(models.Model):
|
||||
""" 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)
|
||||
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,
|
||||
|
@ -434,7 +434,7 @@ class BillLine(models.Model):
|
|||
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||
# Amendment
|
||||
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:
|
||||
get_latest_by = 'id'
|
||||
|
@ -495,7 +495,7 @@ class BillSubline(models.Model):
|
|||
)
|
||||
|
||||
# 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)
|
||||
total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
||||
|
|
|
@ -7,7 +7,7 @@ from orchestra.admin.actions import SendEmail
|
|||
from orchestra.admin.utils import insertattr, change_url
|
||||
from orchestra.contrib.accounts.actions import list_accounts
|
||||
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 .models import Contact
|
||||
|
@ -72,7 +72,7 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
if db_field.name == 'address':
|
||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||
if db_field.name == 'email_usage':
|
||||
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
|
||||
kwargs['widget'] = PaddingCheckboxSelectMultiple(130)
|
||||
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
|
||||
|
@ -101,7 +101,7 @@ class ContactInline(admin.StackedInline):
|
|||
if db_field.name == 'address':
|
||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||
if db_field.name == 'email_usage':
|
||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
||||
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||
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
|
@ -33,7 +33,7 @@ class Contact(models.Model):
|
|||
objects = ContactQuerySet.as_manager()
|
||||
|
||||
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)
|
||||
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
||||
email = models.EmailField()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
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 orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||
|
@ -50,14 +52,14 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
list_prefetch_related = ('users',)
|
||||
actions = (list_accounts, save_selected)
|
||||
|
||||
@mark_safe
|
||||
def display_users(self, db):
|
||||
links = []
|
||||
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)
|
||||
return '<br>'.join(links)
|
||||
display_users.short_description = _("Users")
|
||||
display_users.allow_tags = True
|
||||
display_users.admin_order_field = 'users__username'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
@ -99,14 +101,14 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
|
|||
list_prefetch_related = ('databases',)
|
||||
actions = (list_accounts, save_selected)
|
||||
|
||||
@mark_safe
|
||||
def display_databases(self, user):
|
||||
links = []
|
||||
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)
|
||||
return '<br>'.join(links)
|
||||
display_databases.short_description = _("Databases")
|
||||
display_databases.allow_tags = True
|
||||
display_databases.admin_order_field = 'databases__name'
|
||||
|
||||
def get_urls(self):
|
||||
|
|
|
@ -91,7 +91,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
|||
|
||||
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
||||
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)
|
||||
if 'Invalid' not in original:
|
||||
return original
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
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)),
|
||||
('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)),
|
||||
('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(
|
||||
|
@ -29,7 +30,7 @@ class Migration(migrations.Migration):
|
|||
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
||||
('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)),
|
||||
('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={
|
||||
'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'),
|
||||
),
|
||||
]
|
|
@ -20,8 +20,8 @@ class Database(models.Model):
|
|||
type = models.CharField(_("type"), max_length=32,
|
||||
choices=settings.DATABASES_TYPE_CHOICES,
|
||||
default=settings.DATABASES_DEFAULT_TYPE)
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='databases')
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||
verbose_name=_("Account"), related_name='databases')
|
||||
comments = models.TextField(default="", blank=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -60,8 +60,8 @@ class DatabaseUser(models.Model):
|
|||
type = models.CharField(_("type"), max_length=32,
|
||||
choices=settings.DATABASES_TYPE_CHOICES,
|
||||
default=settings.DATABASES_DEFAULT_TYPE)
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='databaseusers')
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||
verbose_name=_("Account"), related_name='databaseusers')
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = _("DB users")
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import os
|
||||
import socket
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from unittest import skip
|
||||
import MySQLdb
|
||||
from django.conf import settings as djsettings
|
||||
from django.core.management.base import CommandError
|
||||
from django.core.urlresolvers import reverse
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from django.urls import reverse
|
||||
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.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error,
|
||||
snapshot_on_error)
|
||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||
save_response_on_error, snapshot_on_error)
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from ... import backends, settings
|
||||
from ...models import Database, DatabaseUser
|
||||
|
||||
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||
|
||||
|
||||
class DatabaseTestMixin(object):
|
||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
|
||||
|
@ -51,7 +53,6 @@ class DatabaseTestMixin(object):
|
|||
def add_group(self, username, groupname):
|
||||
raise NotImplementedError
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_add(self):
|
||||
dbname = '%s_database' % random_ascii(5)
|
||||
username = '%s_dbuser' % random_ascii(5)
|
||||
|
@ -59,7 +60,6 @@ class DatabaseTestMixin(object):
|
|||
self.add(dbname, username, password)
|
||||
self.validate_create_table(dbname, username, password)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_delete(self):
|
||||
dbname = '%s_database' % random_ascii(5)
|
||||
username = '%s_dbuser' % random_ascii(5)
|
||||
|
@ -71,7 +71,6 @@ class DatabaseTestMixin(object):
|
|||
self.validate_delete(dbname, username, password)
|
||||
self.validate_delete_user(dbname, username)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_change_password(self):
|
||||
dbname = '%s_database' % random_ascii(5)
|
||||
username = '%s_dbuser' % random_ascii(5)
|
||||
|
@ -85,7 +84,6 @@ class DatabaseTestMixin(object):
|
|||
self.validate_login_error(dbname, username, password)
|
||||
self.validate_create_table(dbname, username, new_password)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_add_user(self):
|
||||
dbname = '%s_database' % random_ascii(5)
|
||||
username = '%s_dbuser' % random_ascii(5)
|
||||
|
@ -103,7 +101,6 @@ class DatabaseTestMixin(object):
|
|||
self.validate_create_table(dbname, username, password)
|
||||
self.validate_create_table(dbname, username2, password2)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_delete_user(self):
|
||||
dbname = '%s_database' % random_ascii(5)
|
||||
username = '%s_dbuser' % random_ascii(5)
|
||||
|
@ -123,7 +120,6 @@ class DatabaseTestMixin(object):
|
|||
self.validate_login_error(dbname, username2, password2)
|
||||
self.validate_delete_user(username2, password2)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_swap_user(self):
|
||||
dbname = '%s_database' % random_ascii(5)
|
||||
username = '%s_dbuser' % random_ascii(5)
|
||||
|
@ -187,6 +183,7 @@ class MySQLControllerMixin(object):
|
|||
"""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):
|
||||
def setUp(self):
|
||||
super(RESTDatabaseMixin, self).setUp()
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
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 orchestra.admin import ExtendedModelAdmin
|
||||
|
@ -72,9 +74,8 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
def structured_name(self, domain):
|
||||
if domain.is_top:
|
||||
return domain.name
|
||||
return ' '*4 + domain.name
|
||||
return mark_safe(' '*4 + domain.name)
|
||||
structured_name.short_description = _("name")
|
||||
structured_name.allow_tags = True
|
||||
structured_name.admin_order_field = 'structured_name'
|
||||
|
||||
def display_is_top(self, domain):
|
||||
|
@ -83,6 +84,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
display_is_top.boolean = True
|
||||
display_is_top.admin_order_field = 'top'
|
||||
|
||||
@mark_safe
|
||||
def display_websites(self, domain):
|
||||
if apps.isinstalled('orchestra.contrib.websites'):
|
||||
websites = domain.websites.all()
|
||||
|
@ -92,22 +94,22 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
site_link = get_on_site_link(website.get_absolute_url())
|
||||
admin_url = change_url(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)
|
||||
links.append(link)
|
||||
return '<br>'.join(links)
|
||||
add_url = reverse('admin:websites_website_add')
|
||||
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
||||
image = '<img src="%s"></img>' % static('orchestra/images/add.png')
|
||||
add_link = '<a href="%s" title="%s">%s</a>' % (
|
||||
add_url, _("Add website"), image
|
||||
add_link = format_html(
|
||||
'<a href="{}" title="{}"><img src="{}" /></a>', add_url,
|
||||
_("Add website"), static('orchestra/images/add.png'),
|
||||
)
|
||||
return _("No website %s") % (add_link)
|
||||
return '---'
|
||||
display_websites.admin_order_field = 'websites__name'
|
||||
display_websites.short_description = _("Websites")
|
||||
display_websites.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def display_addresses(self, domain):
|
||||
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
||||
add_url = reverse('admin:mailboxes_address_add')
|
||||
|
@ -126,10 +128,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
return '---'
|
||||
display_addresses.short_description = _("Addresses")
|
||||
display_addresses.admin_order_field = 'addresses__count'
|
||||
display_addresses.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def implicit_records(self, domain):
|
||||
defaults = []
|
||||
types = set(domain.records.values_list('type', flat=True))
|
||||
ttl = settings.DOMAINS_DEFAULT_TTL
|
||||
lines = []
|
||||
|
@ -141,14 +142,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
value=record.value
|
||||
)
|
||||
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:
|
||||
lines.insert(0, line)
|
||||
else:
|
||||
lines.append(line)
|
||||
return '<br>'.join(lines)
|
||||
implicit_records.short_description = _("Implicit records")
|
||||
implicit_records.allow_tags = True
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
""" Add SOA fields when domain is top """
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 orchestra.api import router
|
||||
|
@ -19,7 +19,7 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
|||
qs = super(DomainViewSet, self).get_queryset()
|
||||
return qs.prefetch_related('records')
|
||||
|
||||
@detail_route()
|
||||
@action(detail=True)
|
||||
def view_zone(self, request, pk=None):
|
||||
domain = self.get_object()
|
||||
return Response({
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
import orchestra.contrib.domains.utils
|
||||
import orchestra.contrib.domains.validators
|
||||
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')),
|
||||
('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')),
|
||||
('account', models.ForeignKey(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')),
|
||||
('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(on_delete=django.db.models.deletion.CASCADE, null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
('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')),
|
||||
('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 django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import orchestra.contrib.domains.validators
|
||||
|
||||
|
||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
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(
|
||||
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
|
||||
])
|
||||
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',
|
||||
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,
|
||||
help_text=_("A revision number that changes whenever this domain is updated."))
|
||||
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
|
||||
|
@ -318,7 +318,7 @@ class Record(models.Model):
|
|||
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,
|
||||
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
||||
validators=[validators.validate_zone_interval])
|
||||
|
|
|
@ -2,10 +2,9 @@ import os
|
|||
import time
|
||||
import socket
|
||||
from functools import partial
|
||||
from unittest import skip
|
||||
|
||||
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 orchestra.contrib.orchestration.models import Server, Route
|
||||
|
@ -21,9 +20,9 @@ run = partial(run, display=False)
|
|||
|
||||
class DomainTestMixin(object):
|
||||
MASTER_SERVER = os.environ.get('ORCHESTRA_MASTER_SERVER', 'localhost')
|
||||
SLAVE_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost2')
|
||||
SLAVE_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||
MASTER_SERVER_ADDR = socket.gethostbyname(MASTER_SERVER)
|
||||
SLAVE_SERVER_ADDR = '127.0.0.2'
|
||||
SLAVE_SERVER_ADDR = socket.gethostbyname(SLAVE_SERVER)
|
||||
|
||||
def setUp(self):
|
||||
djsettings.DEBUG = True
|
||||
|
@ -177,7 +176,6 @@ class DomainTestMixin(object):
|
|||
self.assertEqual('CNAME', cname[3])
|
||||
self.assertEqual('external.server.org.', cname[4])
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_add(self):
|
||||
self.add(self.ns1_name, self.ns1_records)
|
||||
self.add(self.ns2_name, self.ns2_records)
|
||||
|
@ -187,7 +185,6 @@ class DomainTestMixin(object):
|
|||
time.sleep(1)
|
||||
self.validate_add(self.SLAVE_SERVER_ADDR, self.domain_name)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_delete(self):
|
||||
self.add(self.ns1_name, self.ns1_records)
|
||||
self.add(self.ns2_name, self.ns2_records)
|
||||
|
@ -197,7 +194,6 @@ class DomainTestMixin(object):
|
|||
self.validate_delete(self.MASTER_SERVER_ADDR, name)
|
||||
self.validate_delete(self.SLAVE_SERVER_ADDR, name)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_update(self):
|
||||
self.add(self.ns1_name, self.ns1_records)
|
||||
self.add(self.ns2_name, self.ns2_records)
|
||||
|
@ -214,7 +210,6 @@ class DomainTestMixin(object):
|
|||
time.sleep(5)
|
||||
self.validate_www_update(self.SLAVE_SERVER_ADDR, self.domain_name)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_add_add_delete_delete(self):
|
||||
self.add(self.ns1_name, self.ns1_records)
|
||||
self.add(self.ns2_name, self.ns2_records)
|
||||
|
@ -227,7 +222,6 @@ class DomainTestMixin(object):
|
|||
self.validate_delete(self.MASTER_SERVER_ADDR, self.django_domain_name)
|
||||
self.validate_delete(self.SLAVE_SERVER_ADDR, self.django_domain_name)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_bad_creation(self):
|
||||
self.assertRaises((self.rest.ResponseStatusError, AssertionError),
|
||||
self.add, self.domain_name, self.domain_records)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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_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):
|
||||
|
@ -34,11 +36,12 @@ class LogEntryAdmin(admin.ModelAdmin):
|
|||
user_link = admin_link('user')
|
||||
display_action_time = admin_date('action_time', short_description=_("Time"))
|
||||
|
||||
@mark_safe
|
||||
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,)),
|
||||
'img': static('admin/img/icon-changelink.svg'),
|
||||
}
|
||||
})
|
||||
if log.is_addition():
|
||||
return _('Added "%(link)s". %(edit)s') % {
|
||||
'link': self.content_object_link(log),
|
||||
|
@ -57,7 +60,6 @@ class LogEntryAdmin(admin.ModelAdmin):
|
|||
}
|
||||
display_message.short_description = _("Message")
|
||||
display_message.admin_order_field = 'action_flag'
|
||||
display_message.allow_tags = True
|
||||
|
||||
def display_action(self, log):
|
||||
if log.is_addition():
|
||||
|
@ -75,10 +77,9 @@ class LogEntryAdmin(admin.ModelAdmin):
|
|||
url = reverse(view, args=(log.object_id,))
|
||||
except NoReverseMatch:
|
||||
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.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):
|
||||
""" Add rel_opts and object to context """
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from django import forms
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
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 markdown import markdown
|
||||
|
||||
|
@ -50,6 +51,7 @@ class MessageReadOnlyInline(admin.TabularInline):
|
|||
'all': ('orchestra/css/hide-inline-id.css',)
|
||||
}
|
||||
|
||||
@mark_safe
|
||||
def content_html(self, msg):
|
||||
context = {
|
||||
'number': msg.number,
|
||||
|
@ -58,12 +60,13 @@ class MessageReadOnlyInline(admin.TabularInline):
|
|||
}
|
||||
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
||||
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
||||
|
||||
content = markdown(msg.content)
|
||||
content = content.replace('>\n', '>')
|
||||
content = '<div style="padding-left:20px;">%s</div>' % content
|
||||
|
||||
return header + content
|
||||
content_html.short_description = _("Content")
|
||||
content_html.allow_tags = True
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
@ -111,10 +114,10 @@ class TicketInline(admin.TabularInline):
|
|||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
@mark_safe
|
||||
def ticket_id(self, instance):
|
||||
return '<b>%s</b>' % admin_link()(instance)
|
||||
ticket_id.short_description = '#'
|
||||
ticket_id.allow_tags = True
|
||||
|
||||
|
||||
class TicketAdmin(ExtendedModelAdmin):
|
||||
|
@ -135,7 +138,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
|||
'owner__username'
|
||||
)
|
||||
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
|
||||
)
|
||||
sudo_actions = ('delete_selected',)
|
||||
|
@ -192,6 +195,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
|||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
@mark_safe
|
||||
def display_summary(self, ticket):
|
||||
context = {
|
||||
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
||||
|
@ -207,14 +211,12 @@ class TicketAdmin(ExtendedModelAdmin):
|
|||
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
||||
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
||||
display_summary.short_description = 'Summary'
|
||||
display_summary.allow_tags = True
|
||||
|
||||
def unbold_id(self, ticket):
|
||||
""" Unbold id if ticket is read """
|
||||
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
|
||||
unbold_id.allow_tags = True
|
||||
unbold_id.short_description = "#"
|
||||
unbold_id.admin_order_field = 'id'
|
||||
|
||||
|
@ -222,8 +224,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
|||
""" Bold subject when tickets are unread for request.user """
|
||||
if ticket.is_read_by(self.user):
|
||||
return ticket.subject
|
||||
return "<strong class='unread'>%s</strong>" % ticket.subject
|
||||
bold_subject.allow_tags = True
|
||||
return format_html("<strong class='unread'>{}</strong>", ticket.subject)
|
||||
bold_subject.short_description = _("Subject")
|
||||
bold_subject.admin_order_field = 'subject'
|
||||
|
||||
|
@ -297,10 +298,9 @@ class QueueAdmin(admin.ModelAdmin):
|
|||
num = queue.tickets__count
|
||||
url = reverse('admin:issues_ticket_changelist')
|
||||
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.admin_order_field = 'tickets__count'
|
||||
num_tickets.allow_tags = True
|
||||
|
||||
def get_list_display(self, request):
|
||||
""" show notifications """
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 orchestra.api import router, LogApiMixin
|
||||
|
@ -13,13 +13,13 @@ class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
|
|||
queryset = Ticket.objects.all()
|
||||
serializer_class = TicketSerializer
|
||||
|
||||
@detail_route()
|
||||
@action(detail=True)
|
||||
def mark_as_read(self, request, pk=None):
|
||||
ticket = self.get_object()
|
||||
ticket.mark_as_read_by(request.user)
|
||||
return Response({'status': 'Ticket marked as read'})
|
||||
|
||||
@detail_route()
|
||||
@action(detail=True)
|
||||
def mark_as_unread(self, request, pk=None):
|
||||
ticket = self.get_object()
|
||||
ticket.mark_as_unread_by(request.user)
|
||||
|
|
|
@ -22,7 +22,7 @@ class MarkDownWidget(forms.Textarea):
|
|||
)
|
||||
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
|
||||
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
||||
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import models, migrations
|
||||
import orchestra.models.fields
|
||||
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')),
|
||||
('content', models.TextField(verbose_name='content')),
|
||||
('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={
|
||||
'get_latest_by': 'id',
|
||||
|
@ -48,9 +49,9 @@ class Migration(migrations.Migration):
|
|||
('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(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')),
|
||||
('queue', models.ForeignKey(blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
||||
('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(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(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-updated_at'],
|
||||
|
@ -60,14 +61,14 @@ class Migration(migrations.Migration):
|
|||
name='TicketTracker',
|
||||
fields=[
|
||||
('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')),
|
||||
('user', models.ForeignKey(related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
('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(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(
|
||||
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'),
|
||||
),
|
||||
]
|
|
@ -161,10 +161,10 @@ class Ticket(models.Model):
|
|||
|
||||
|
||||
class Message(models.Model):
|
||||
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
|
||||
related_name='messages')
|
||||
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"),
|
||||
related_name='ticket_messages')
|
||||
ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
|
||||
verbose_name=_("ticket"), related_name='messages')
|
||||
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||
verbose_name=_("author"), related_name='ticket_messages')
|
||||
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
||||
content = models.TextField(_("content"))
|
||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||
|
@ -191,9 +191,10 @@ class Message(models.Model):
|
|||
|
||||
class TicketTracker(models.Model):
|
||||
""" Keeps track of user read tickets """
|
||||
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers')
|
||||
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"),
|
||||
related_name='ticket_trackers')
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
|
||||
verbose_name=_("ticket"), related_name='trackers')
|
||||
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||
verbose_name=_("user"), related_name='ticket_trackers')
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
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)),
|
||||
('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.')),
|
||||
('account', models.ForeignKey(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')),
|
||||
('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(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
||||
],
|
||||
),
|
||||
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,7 +30,7 @@ class List(models.Model):
|
|||
admin_email = models.EmailField(_("admin email"),
|
||||
help_text=_("Administration email address"))
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='lists')
|
||||
related_name='lists', on_delete=models.CASCADE)
|
||||
# TODO also admin
|
||||
is_active = models.BooleanField(_("active"), default=True,
|
||||
help_text=_("Designates whether this account should be treated as active. "
|
||||
|
|
|
@ -12,7 +12,7 @@ from .models import List
|
|||
|
||||
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = List.address_domain.field.rel.to
|
||||
model = List.address_domain.field.related_model
|
||||
fields = ('url', 'id', 'name')
|
||||
|
||||
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
import os
|
||||
import smtplib
|
||||
import time
|
||||
import requests
|
||||
from unittest import skip
|
||||
import unittest
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import requests
|
||||
from django.conf import settings as djsettings
|
||||
from django.core.management.base import CommandError
|
||||
from django.core.urlresolvers import reverse
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from django.urls import reverse
|
||||
from orchestra.admin.utils import change_url
|
||||
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.tests import (BaseLiveServerTestCase, random_ascii, snapshot_on_error,
|
||||
save_response_on_error)
|
||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||
save_response_on_error, snapshot_on_error)
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from ... import backends, settings
|
||||
from ...models import List
|
||||
|
||||
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||
|
||||
|
||||
class ListMixin(object):
|
||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||
|
@ -31,11 +32,8 @@ class ListMixin(object):
|
|||
|
||||
def setUp(self):
|
||||
super(ListMixin, self).setUp()
|
||||
djsettings.DEBUG = True
|
||||
djsettings.CELERY_ALWAYS_EAGER = True
|
||||
djsettings.CELERY_TASK_ALWAYS_EAGER = True
|
||||
# import pdb; pdb.set_trace()
|
||||
self.add_route()
|
||||
djsettings.DEBUG = True
|
||||
|
||||
def validate_add(self, name, address=None):
|
||||
sshrun(self.MASTER_SERVER, 'list_members %s' % name, display=False)
|
||||
|
@ -86,7 +84,6 @@ class ListMixin(object):
|
|||
backend = backends.MailmanController.get_name()
|
||||
Route.objects.create(backend=backend, match=True, host=server)
|
||||
|
||||
# @skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_add(self):
|
||||
name = '%s_list' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -96,7 +93,6 @@ class ListMixin(object):
|
|||
self.validate_login(name, password)
|
||||
self.addCleanup(self.delete, name)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_add_with_address(self):
|
||||
name = '%s_list' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -109,7 +105,6 @@ class ListMixin(object):
|
|||
# Mailman doesn't support changing the address, only the domain
|
||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_change_password(self):
|
||||
name = '%s_list' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -121,7 +116,6 @@ class ListMixin(object):
|
|||
self.change_password(name, new_password)
|
||||
self.validate_login(name, new_password)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_change_domain(self):
|
||||
name = '%s_list' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -137,7 +131,6 @@ class ListMixin(object):
|
|||
self.update_domain(name, domain_name)
|
||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_change_address_name(self):
|
||||
name = '%s_list' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -152,7 +145,6 @@ class ListMixin(object):
|
|||
self.update_address_name(name, address_name)
|
||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_delete(self):
|
||||
name = '%s_list' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -160,8 +152,7 @@ class ListMixin(object):
|
|||
address_name = '%s_name' % random_ascii(10)
|
||||
domain_name = '%sdomain.lan' % random_ascii(10)
|
||||
address_domain = Domain.objects.create(name=domain_name, account=self.account)
|
||||
self.add(name, password, admin_email, address_name=address_name,
|
||||
address_domain=address_domain)
|
||||
self.add(name, password, admin_email, address_name=address_name, address_domain=address_domain)
|
||||
# Mailman doesn't support changing the address, only the domain
|
||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||
self.delete(name)
|
||||
|
@ -169,6 +160,7 @@ class ListMixin(object):
|
|||
self.validate_delete(name)
|
||||
|
||||
|
||||
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||
class RESTListMixin(ListMixin):
|
||||
def setUp(self):
|
||||
super(RESTListMixin, self).setUp()
|
||||
|
@ -236,8 +228,6 @@ class AdminListMixin(ListMixin):
|
|||
domain_select.select_by_value(str(domain.pk))
|
||||
|
||||
name_field.submit()
|
||||
# import pdb; pdb.set_trace()
|
||||
# oop = Server.objects.all()
|
||||
self.assertNotEqual(url, self.selenium.current_url)
|
||||
|
||||
@snapshot_on_error
|
||||
|
|
|
@ -3,9 +3,10 @@ from urllib.parse import parse_qs
|
|||
|
||||
from django import forms
|
||||
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.functions import Concat
|
||||
from django.utils.html import format_html, format_html_join
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -82,6 +83,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
|||
if settings.MAILBOXES_LOCAL_DOMAIN:
|
||||
type(self).actions = self.actions + (SendMailboxEmail(),)
|
||||
|
||||
@mark_safe
|
||||
def display_addresses(self, mailbox):
|
||||
# Get from forwards
|
||||
cache = caches.get_request_cache()
|
||||
|
@ -93,7 +95,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
|||
qs = qs.values_list('id', 'email', 'forward')
|
||||
for addr_id, email, mbox in qs:
|
||||
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:
|
||||
cached_forwards[mbox].append(link)
|
||||
except KeyError:
|
||||
|
@ -107,26 +109,23 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
|||
addresses = []
|
||||
for addr in mailbox.addresses.all():
|
||||
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)
|
||||
display_addresses.short_description = _("Addresses")
|
||||
display_addresses.allow_tags = True
|
||||
|
||||
def display_forwards(self, mailbox):
|
||||
forwards = []
|
||||
for addr in mailbox.get_forwards():
|
||||
url = change_url(addr)
|
||||
forwards.append('<a href="%s">%s</a>' % (url, addr.email))
|
||||
return '<br>'.join(forwards)
|
||||
forwards = mailbox.get_forwards()
|
||||
return format_html_join(
|
||||
'<br>', '<a href="{}">{}</a>',
|
||||
[(change_url(addr), addr.email) for addr in forwards]
|
||||
)
|
||||
display_forwards.short_description = _("Forward from")
|
||||
display_forwards.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def display_filtering(self, mailbox):
|
||||
""" becacuse of allow_tags = True """
|
||||
return mailbox.get_filtering_display()
|
||||
display_filtering.short_description = _("Filtering")
|
||||
display_filtering.admin_order_field = 'filtering'
|
||||
display_filtering.allow_tags = True
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
if db_field.name == 'filtering':
|
||||
|
@ -217,7 +216,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
|||
elif obj.custom_filtering:
|
||||
messages.warning(request, msg)
|
||||
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):
|
||||
|
@ -247,29 +246,27 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
|
||||
def email_link(self, 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.allow_tags = True
|
||||
|
||||
def display_mailboxes(self, address):
|
||||
boxes = []
|
||||
for mailbox in address.mailboxes.all():
|
||||
url = change_url(mailbox)
|
||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
||||
return '<br>'.join(boxes)
|
||||
boxes = address.mailboxes.all()
|
||||
return format_html_join(
|
||||
mark_safe('<br>'), '<a href="{}">{}</a>',
|
||||
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
|
||||
)
|
||||
display_mailboxes.short_description = _("Mailboxes")
|
||||
display_mailboxes.allow_tags = True
|
||||
display_mailboxes.admin_order_field = 'mailboxes__count'
|
||||
|
||||
def display_all_mailboxes(self, address):
|
||||
boxes = []
|
||||
for mailbox in address.get_mailboxes():
|
||||
url = change_url(mailbox)
|
||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
||||
return '<br>'.join(boxes)
|
||||
boxes = address.get_mailboxes()
|
||||
return format_html_join(
|
||||
mark_safe('<br>'), '<a href="{}">{}</a>',
|
||||
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
|
||||
)
|
||||
display_all_mailboxes.short_description = _("Mailboxes links")
|
||||
display_all_mailboxes.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def display_forward(self, address):
|
||||
forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()}
|
||||
values = []
|
||||
|
@ -281,7 +278,6 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
values.append(forward)
|
||||
return '<br>'.join(values)
|
||||
display_forward.short_description = _("Forward")
|
||||
display_forward.allow_tags = True
|
||||
display_forward.admin_order_field = 'forward'
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
|
|
|
@ -4,7 +4,7 @@ from orchestra.api import router, SetPasswordApiMixin, LogApiMixin
|
|||
from orchestra.contrib.accounts.api import AccountApiMixin
|
||||
|
||||
from .models import Address, Mailbox
|
||||
from .serializers import AddressSerializer, MailboxSerializer
|
||||
from .serializers import AddressSerializer, MailboxSerializer, MailboxWritableSerializer
|
||||
|
||||
|
||||
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||
|
@ -17,6 +17,12 @@ class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets
|
|||
queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
|
||||
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'addresses', AddressViewSet)
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
import orchestra.contrib.mailboxes.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')),
|
||||
('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)),
|
||||
('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
|
||||
('domain', models.ForeignKey(to='domains.Domain', related_name='addresses', verbose_name='domain')),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Domain', related_name='addresses', verbose_name='domain')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'addresses',
|
||||
|
@ -35,7 +36,7 @@ class Migration(migrations.Migration):
|
|||
('subject', models.CharField(verbose_name='subject', max_length=256)),
|
||||
('message', models.TextField(verbose_name='message')),
|
||||
('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(
|
||||
|
@ -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')),
|
||||
('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)),
|
||||
('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={
|
||||
'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')]),
|
||||
),
|
||||
]
|
|
@ -23,7 +23,7 @@ class Mailbox(models.Model):
|
|||
])
|
||||
password = models.CharField(_("password"), max_length=128)
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||
related_name='mailboxes')
|
||||
related_name='mailboxes', on_delete=models.CASCADE)
|
||||
filtering = models.CharField(max_length=16,
|
||||
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
|
||||
choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())])
|
||||
|
@ -44,7 +44,7 @@ class Mailbox(models.Model):
|
|||
def active(self):
|
||||
try:
|
||||
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
|
||||
|
||||
def disable(self):
|
||||
|
@ -97,14 +97,14 @@ class Address(models.Model):
|
|||
validators=[validators.validate_emailname],
|
||||
help_text=_("Address name, left blank for a <i>catch-all</i> address"))
|
||||
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"),
|
||||
related_name='addresses', blank=True)
|
||||
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
||||
validators=[validators.validate_forward],
|
||||
help_text=_("Space separated email addresses or mailboxes"))
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='addresses')
|
||||
related_name='addresses', on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = _("addresses")
|
||||
|
@ -168,7 +168,7 @@ class Address(models.Model):
|
|||
|
||||
class Autoresponse(models.Model):
|
||||
address = models.OneToOneField(Address, verbose_name=_("address"),
|
||||
related_name='autoresponse')
|
||||
related_name='autoresponse', on_delete=models.CASCADE)
|
||||
# TODO initial_date
|
||||
subject = models.CharField(_("subject"), max_length=256)
|
||||
message = models.TextField(_("message"))
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from django.db import transaction
|
||||
from rest_framework import serializers
|
||||
|
||||
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer
|
||||
|
@ -8,7 +9,7 @@ from .models import Mailbox, Address
|
|||
|
||||
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Address.domain.field.rel.to
|
||||
model = Address.domain.field.related_model
|
||||
fields = ('url', 'id', 'name')
|
||||
|
||||
|
||||
|
@ -35,6 +36,41 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer
|
|||
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 Meta:
|
||||
model = Mailbox
|
||||
|
@ -43,7 +79,7 @@ class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSe
|
|||
|
||||
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
domain = RelatedDomainSerializer()
|
||||
mailboxes = RelatedMailboxSerializer(many=True, required=False) #allow_add_remove=True
|
||||
mailboxes = RelatedMailboxSerializer(many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Address
|
||||
|
@ -51,6 +87,21 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri
|
|||
|
||||
def validate(self, 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.")
|
||||
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']
|
||||
local_domain = settings.MAILBOXES_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:
|
||||
domain = Domain.objects.get(name=local_domain)
|
||||
except Domain.DoesNotExist:
|
||||
|
|
|
@ -4,14 +4,14 @@ import poplib
|
|||
import smtplib
|
||||
import time
|
||||
import textwrap
|
||||
import unittest
|
||||
from email.mime.text import MIMEText
|
||||
from unittest import skip
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings as djsettings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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.contrib.orchestration.models import Server, Route
|
||||
|
@ -22,6 +22,8 @@ from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot
|
|||
from ... import backends, settings
|
||||
from ...models import Mailbox
|
||||
|
||||
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||
|
||||
|
||||
class MailboxMixin(object):
|
||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||
|
@ -49,6 +51,7 @@ class MailboxMixin(object):
|
|||
Resource.objects.create(
|
||||
name='disk',
|
||||
content_type=ContentType.objects.get_for_model(Mailbox),
|
||||
period=Resource.LAST,
|
||||
verbose_name='Mail quota',
|
||||
unit='MB',
|
||||
scale=10**6,
|
||||
|
@ -108,7 +111,6 @@ class MailboxMixin(object):
|
|||
home = Mailbox.objects.get(name=username).get_home()
|
||||
sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_add(self):
|
||||
username = '%s_mailbox' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -117,7 +119,6 @@ class MailboxMixin(object):
|
|||
imap = self.login_imap(username, password)
|
||||
self.validate_mailbox(username)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_change_password(self):
|
||||
username = '%s_systemuser' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -128,7 +129,6 @@ class MailboxMixin(object):
|
|||
self.change_password(username, new_password)
|
||||
imap = self.login_imap(username, new_password)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_quota(self):
|
||||
username = '%s_mailbox' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -143,7 +143,6 @@ class MailboxMixin(object):
|
|||
imap_quota = int(imap.getquotaroot("INBOX")[1][1][0].split(' ')[-1].split(')')[0])
|
||||
self.assertEqual(quota*1024, imap_quota)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_send_email(self):
|
||||
username = '%s_mailbox' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -160,7 +159,6 @@ class MailboxMixin(object):
|
|||
finally:
|
||||
server.quit()
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_address(self):
|
||||
username = '%s_mailbox' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -174,7 +172,6 @@ class MailboxMixin(object):
|
|||
self.send_email("%s@%s" % (name, domain), token)
|
||||
self.validate_email(username, token)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_disable(self):
|
||||
username = '%s_systemuser' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -185,7 +182,6 @@ class MailboxMixin(object):
|
|||
self.disable(username)
|
||||
self.assertRaises(imap.error, self.login_imap, username, password)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_delete(self):
|
||||
username = '%s_systemuser' % random_ascii(10)
|
||||
password = '@!?%sppppP001' % random_ascii(5)
|
||||
|
@ -201,7 +197,6 @@ class MailboxMixin(object):
|
|||
self.assertRaises(CommandError,
|
||||
sshrun, self.MASTER_SERVER, 'ls %s' % home, display=False)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_delete_address(self):
|
||||
username = '%s_mailbox' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -218,7 +213,6 @@ class MailboxMixin(object):
|
|||
self.send_email("%s@%s" % (name, domain), token)
|
||||
self.validate_email(username, token)
|
||||
|
||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
||||
def test_custom_filtering(self):
|
||||
username = '%s_mailbox' % random_ascii(10)
|
||||
password = '@!?%spppP001' % random_ascii(5)
|
||||
|
@ -244,6 +238,7 @@ class MailboxMixin(object):
|
|||
# TODO test autoreply
|
||||
|
||||
|
||||
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||
class RESTMailboxMixin(MailboxMixin):
|
||||
def setUp(self):
|
||||
super(RESTMailboxMixin, self).setUp()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
|
|
|
@ -3,9 +3,11 @@ import email
|
|||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db.models import Count
|
||||
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 orchestra.admin import ExtendedModelAdmin
|
||||
|
@ -60,11 +62,10 @@ class MessageAdmin(ExtendedModelAdmin):
|
|||
def display_subject(self, instance):
|
||||
subject = instance.subject
|
||||
if len(subject) > 64:
|
||||
return subject[:64] + '…'
|
||||
return mark_safe(subject[:64] + '…')
|
||||
return subject
|
||||
display_subject.short_description = _("Subject")
|
||||
display_subject.admin_order_field = 'subject'
|
||||
display_subject.allow_tags = True
|
||||
|
||||
def display_retries(self, instance):
|
||||
num_logs = instance.logs__count
|
||||
|
@ -74,10 +75,9 @@ class MessageAdmin(ExtendedModelAdmin):
|
|||
else:
|
||||
url = reverse('admin:mailer_smtplog_changelist')
|
||||
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.admin_order_field = 'retries'
|
||||
display_retries.allow_tags = True
|
||||
|
||||
def display_content(self, instance):
|
||||
part = email.message_from_string(instance.content)
|
||||
|
@ -99,9 +99,8 @@ class MessageAdmin(ExtendedModelAdmin):
|
|||
payload = payload.decode(charset)
|
||||
if part.get_content_type() == 'text/plain':
|
||||
payload = payload.replace('\n', '<br>').replace(' ', ' ')
|
||||
return payload
|
||||
return mark_safe(payload)
|
||||
display_content.short_description = _("Content")
|
||||
display_content.allow_tags = True
|
||||
|
||||
def display_full_subject(self, instance):
|
||||
return instance.subject
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
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)),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('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'),
|
||||
),
|
||||
]
|
|
@ -67,7 +67,7 @@ class SMTPLog(models.Model):
|
|||
(SUCCESS, _("Success")),
|
||||
(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)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
log_message = models.TextField()
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -38,15 +39,13 @@ class MiscServiceAdmin(ExtendedModelAdmin):
|
|||
actions = (disable, enable)
|
||||
|
||||
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.allow_tags = True
|
||||
display_name.admin_order_field = 'name'
|
||||
|
||||
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.allow_tags = True
|
||||
display_verbose_name.admin_order_field = 'verbose_name'
|
||||
|
||||
def num_instances(self, misc):
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
import orchestra.core.validators
|
||||
from django.conf import settings
|
||||
import orchestra.models.fields
|
||||
|
@ -22,7 +23,7 @@ class Migration(migrations.Migration):
|
|||
('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(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={
|
||||
'verbose_name_plural': 'miscellaneous',
|
||||
|
@ -43,6 +44,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='miscellaneous',
|
||||
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'),
|
||||
),
|
||||
]
|
|
@ -42,10 +42,10 @@ class MiscService(models.Model):
|
|||
|
||||
|
||||
class Miscellaneous(models.Model):
|
||||
service = models.ForeignKey(MiscService, verbose_name=_("service"),
|
||||
related_name='instances')
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||
related_name='miscellaneous')
|
||||
service = models.ForeignKey(MiscService, on_delete=models.CASCADE,
|
||||
verbose_name=_("service"), related_name='instances')
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||
verbose_name=_("account"), related_name='miscellaneous')
|
||||
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
|
||||
db_index=True, help_text=_("A unique identifier for this service."))
|
||||
description = models.TextField(_("description"), blank=True)
|
||||
|
|
|
@ -39,10 +39,10 @@ class Operation():
|
|||
self.routes = routes
|
||||
|
||||
@classmethod
|
||||
def execute(cls, operations, serialize=False, async=None):
|
||||
def execute(cls, operations, serialize=False, run_async=None):
|
||||
from . import manager
|
||||
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
|
||||
def create_for_action(cls, instances, action):
|
||||
|
|
|
@ -30,14 +30,14 @@ STATE_COLORS = {
|
|||
|
||||
class RouteAdmin(ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'async',
|
||||
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'run_async',
|
||||
'is_active'
|
||||
)
|
||||
list_editable = ('host', 'match', 'async', 'is_active')
|
||||
list_filter = ('host', 'is_active', 'async', 'backend')
|
||||
list_editable = ('host', 'match', 'run_async', 'is_active')
|
||||
list_filter = ('host', 'is_active', 'run_async', 'backend')
|
||||
list_prefetch_related = ('host',)
|
||||
ordering = ('backend',)
|
||||
add_fields = ('backend', 'host', 'match', 'async', 'is_active')
|
||||
add_fields = ('backend', 'host', 'match', 'run_async', 'is_active')
|
||||
change_form = RouteForm
|
||||
actions = (orchestrate,)
|
||||
change_view_actions = actions
|
||||
|
@ -51,19 +51,18 @@ class RouteAdmin(ExtendedModelAdmin):
|
|||
|
||||
def display_model(self, route):
|
||||
try:
|
||||
return escape(route.backend_class.model)
|
||||
return route.backend_class.model
|
||||
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.allow_tags = True
|
||||
|
||||
@mark_safe
|
||||
def display_actions(self, route):
|
||||
try:
|
||||
return '<br>'.join(route.backend_class.get_actions())
|
||||
except KeyError:
|
||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
||||
display_actions.short_description = _("actions")
|
||||
display_actions.allow_tags = True
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Provides dynamic help text on backend form field """
|
||||
|
@ -120,7 +119,6 @@ class BackendOperationInline(admin.TabularInline):
|
|||
return _("Deleted {0}").format(operation.instance_repr or '-'.join(
|
||||
(escape(operation.content_type), escape(operation.object_id))))
|
||||
return link
|
||||
instance_link.allow_tags = True
|
||||
instance_link.short_description = _("Instance")
|
||||
|
||||
def has_add_permission(self, *args, **kwargs):
|
||||
|
@ -179,14 +177,12 @@ class ServerAdmin(ExtendedModelAdmin):
|
|||
change_view_actions = actions
|
||||
|
||||
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.allow_tags = True
|
||||
|
||||
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.allow_tags = True
|
||||
|
||||
def get_queryset(self, request):
|
||||
""" Order by structured name and imporve performance """
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue