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 -U postgres
|
||||||
psql (12.4)
|
psql (12.4)
|
||||||
Digite «help».
|
Digite «help» para obtener ayuda.
|
||||||
|
|
||||||
postgres=# CREATE database orchesta;
|
postgres=# CREATE database orchesta;
|
||||||
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';
|
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';
|
||||||
|
|
|
@ -3,7 +3,7 @@ from collections import OrderedDict
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from fluent_dashboard import dashboard, appsettings
|
from fluent_dashboard import dashboard, appsettings
|
||||||
from fluent_dashboard.modules import CmsAppIconList
|
from fluent_dashboard.modules import CmsAppIconList
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.forms.models import modelformset_factory, BaseModelFormSet
|
from django.forms.models import modelformset_factory, BaseModelFormSet
|
||||||
from django.template import Template, Context
|
from django.template import Template
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.forms.widgets import SpanWidget
|
from orchestra.forms.widgets import SpanWidget
|
||||||
|
@ -28,9 +28,9 @@ class AdminFormMixin(object):
|
||||||
' {% include "admin/includes/fieldset.html" %}'
|
' {% include "admin/includes/fieldset.html" %}'
|
||||||
'{% endfor %}'
|
'{% endfor %}'
|
||||||
)
|
)
|
||||||
context = Context({
|
context = {
|
||||||
'adminform': adminform
|
'adminform': adminform
|
||||||
})
|
}
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,9 +71,9 @@ class AdminFormSet(BaseModelFormSet):
|
||||||
</div>
|
</div>
|
||||||
</div>""")
|
</div>""")
|
||||||
)
|
)
|
||||||
context = Context({
|
context = {
|
||||||
'formset': self
|
'formset': self
|
||||||
})
|
}
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from admin_tools.menu import items, Menu
|
from admin_tools.menu import items, Menu
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,11 @@ from functools import wraps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from orchestra.models.utils import get_field_value
|
from orchestra.models.utils import get_field_value
|
||||||
|
@ -113,21 +113,21 @@ def admin_link(*args, **kwargs):
|
||||||
return '---'
|
return '---'
|
||||||
if not getattr(obj, 'pk', None):
|
if not getattr(obj, 'pk', None):
|
||||||
return '---'
|
return '---'
|
||||||
display = kwargs.get('display')
|
display_ = kwargs.get('display')
|
||||||
if display:
|
if display_:
|
||||||
display = getattr(obj, display, display)
|
display_ = getattr(obj, display_, display_)
|
||||||
else:
|
else:
|
||||||
display = obj
|
display_ = obj
|
||||||
try:
|
try:
|
||||||
url = change_url(obj)
|
url = change_url(obj)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
# Does not has admin
|
# Does not has admin
|
||||||
return str(display)
|
return str(display_)
|
||||||
extra = ''
|
extra = ''
|
||||||
if kwargs['popup']:
|
if kwargs['popup']:
|
||||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
extra = mark_safe('onclick="return showAddAnotherPopup(this);"')
|
||||||
title = "Change %s" % obj._meta.verbose_name
|
title = "Change %s" % obj._meta.verbose_name
|
||||||
return mark_safe('<a href="%s" title="%s" %s>%s</a>' % (url, title, extra, display))
|
return format_html('<a href="{}" title="{}" {}>{}</a>', url, title, extra, display_)
|
||||||
|
|
||||||
|
|
||||||
@admin_field
|
@admin_field
|
||||||
|
@ -158,7 +158,7 @@ def admin_date(*args, **kwargs):
|
||||||
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
else:
|
else:
|
||||||
date = date.strftime("%Y-%m-%d")
|
date = date.strftime("%Y-%m-%d")
|
||||||
return '<span title="{0}">{1}</span>'.format(date, escape(natural))
|
return format_html('<span title="{0}">{1}</span>', date, natural)
|
||||||
|
|
||||||
|
|
||||||
def get_object_from_url(modeladmin, request):
|
def get_object_from_url(modeladmin, request):
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .serializers import SetPasswordSerializer
|
from .serializers import SetPasswordSerializer
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordApiMixin(object):
|
class SetPasswordApiMixin(object):
|
||||||
@detail_route(methods=['post'], serializer_class=SetPasswordSerializer)
|
@action(detail=True, methods=['post'], serializer_class=SetPasswordSerializer)
|
||||||
def set_password(self, request, pk):
|
def set_password(self, request, pk):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
data = request.data
|
data = request.data
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,16 +23,16 @@ def link_wrap(view, view_names):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def insert_links(viewset, base_name):
|
def insert_links(viewset, basename):
|
||||||
collection_links = ['api-root', '%s-list' % base_name]
|
collection_links = ['api-root', '%s-list' % basename]
|
||||||
object_links = ['api-root', '%s-list' % base_name, '%s-detail' % base_name]
|
object_links = ['api-root', '%s-list' % basename, '%s-detail' % basename]
|
||||||
exception_links = ['api-root']
|
exception_links = ['api-root']
|
||||||
list_links = ['api-root']
|
list_links = ['api-root']
|
||||||
retrieve_links = ['api-root', '%s-list' % base_name]
|
retrieve_links = ['api-root', '%s-list' % basename]
|
||||||
# Determine any `@action` or `@link` decorated methods on the viewset
|
# Determine any `@action` or `@link` decorated methods on the viewset
|
||||||
for methodname in dir(viewset):
|
for methodname in dir(viewset):
|
||||||
method = getattr(viewset, methodname)
|
method = getattr(viewset, methodname)
|
||||||
view_name = '%s-%s' % (base_name, methodname.replace('_', '-'))
|
view_name = '%s-%s' % (basename, methodname.replace('_', '-'))
|
||||||
if hasattr(method, 'collection_bind_to_methods'):
|
if hasattr(method, 'collection_bind_to_methods'):
|
||||||
list_links.append(view_name)
|
list_links.append(view_name)
|
||||||
retrieve_links.append(view_name)
|
retrieve_links.append(view_name)
|
||||||
|
|
|
@ -65,12 +65,12 @@ class LinkHeaderRouter(DefaultRouter):
|
||||||
APIRoot.router = self
|
APIRoot.router = self
|
||||||
return APIRoot.as_view()
|
return APIRoot.as_view()
|
||||||
|
|
||||||
def register(self, prefix, viewset, base_name=None):
|
def register(self, prefix, viewset, basename=None):
|
||||||
""" inserts link headers on every viewset """
|
""" inserts link headers on every viewset """
|
||||||
if base_name is None:
|
if basename is None:
|
||||||
base_name = self.get_default_base_name(viewset)
|
basename = self.get_default_basename(viewset)
|
||||||
insert_links(viewset, base_name)
|
insert_links(viewset, basename)
|
||||||
self.registry.append((prefix, viewset, base_name))
|
self.registry.append((prefix, viewset, basename))
|
||||||
|
|
||||||
def get_viewset(self, prefix_or_model):
|
def get_viewset(self, prefix_or_model):
|
||||||
for _prefix, viewset, __ in self.registry:
|
for _prefix, viewset, __ in self.registry:
|
||||||
|
|
|
@ -23,7 +23,7 @@ class APIRoot(views.APIView):
|
||||||
'accountancy': {},
|
'accountancy': {},
|
||||||
'services': {},
|
'services': {},
|
||||||
}
|
}
|
||||||
if not request.user.is_anonymous():
|
if not request.user.is_anonymous:
|
||||||
list_name = '{basename}-list'
|
list_name = '{basename}-list'
|
||||||
detail_name = '{basename}-detail'
|
detail_name = '{basename}-detail'
|
||||||
for prefix, viewset, basename in self.router.registry:
|
for prefix, viewset, basename in self.router.registry:
|
||||||
|
|
|
@ -64,7 +64,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
|
||||||
""" returns object on to_internal_value based on URL """
|
""" returns object on to_internal_value based on URL """
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
try:
|
||||||
url = data.get('url')
|
url = data.get('url')
|
||||||
|
except AttributeError:
|
||||||
|
url = None
|
||||||
if not url:
|
if not url:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'url': "URL is required."
|
'url': "URL is required."
|
||||||
|
@ -81,14 +84,14 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
|
||||||
validators=[validate_password], write_only=True, required=False,
|
validators=[validate_password], write_only=True, required=False,
|
||||||
style={'widget': widgets.PasswordInput})
|
style={'widget': widgets.PasswordInput})
|
||||||
|
|
||||||
def validate_password(self, attrs, source):
|
def validate_password(self, value):
|
||||||
""" POST only password """
|
""" POST only password """
|
||||||
if self.instance:
|
if self.instance:
|
||||||
if 'password' in attrs:
|
if value:
|
||||||
raise serializers.ValidationError(_("Can not set password"))
|
raise serializers.ValidationError(_("Can not set password"))
|
||||||
elif 'password' not in attrs:
|
elif not value:
|
||||||
raise serializers.ValidationError(_("Password required"))
|
raise serializers.ValidationError(_("Password required"))
|
||||||
return attrs
|
return value
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
""" remove password in case is not a real model field """
|
""" remove password in case is not a real model field """
|
||||||
|
|
|
@ -116,17 +116,20 @@ function install_requirements () {
|
||||||
update-locale LANG=en_US.UTF-8
|
update-locale LANG=en_US.UTF-8
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# lxml: libxml2-dev, libxslt1-dev, zlib1g-dev
|
||||||
APT="bind9utils \
|
APT="bind9utils \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
gettext \
|
gettext \
|
||||||
libcrack2-dev \
|
libcrack2-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
python3 \
|
python3 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
python3-lxml \
|
|
||||||
ssh-client \
|
ssh-client \
|
||||||
wget \
|
wget \
|
||||||
xvfb"
|
xvfb \
|
||||||
|
zlib1g-dev"
|
||||||
if $testing; then
|
if $testing; then
|
||||||
APT="${APT} \
|
APT="${APT} \
|
||||||
git \
|
git \
|
||||||
|
@ -147,13 +150,14 @@ function install_requirements () {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# cracklib and lxml are excluded on the requirements.txt because they need unconvinient system dependencies
|
# cracklib and lxml are excluded on the requirements.txt because they need unconvinient system dependencies
|
||||||
PIP="$(wget https://raw.githubusercontent.com/ribaguifi/django-orchestra/dev/github-actions/requirements.txt -O - | tr '\n' ' ') \
|
PIP="$(wget http://git.io/orchestra-requirements.txt -O - | tr '\n' ' ') \
|
||||||
cracklib"
|
cracklib \
|
||||||
|
lxml==3.3.5"
|
||||||
if $testing; then
|
if $testing; then
|
||||||
PIP="${PIP} \
|
PIP="${PIP} \
|
||||||
selenium \
|
selenium \
|
||||||
xvfbwrapper \
|
xvfbwrapper \
|
||||||
freezegun \
|
freezegun==0.3.14 \
|
||||||
coverage \
|
coverage \
|
||||||
flake8 \
|
flake8 \
|
||||||
django-debug-toolbar==1.3.0 \
|
django-debug-toolbar==1.3.0 \
|
||||||
|
|
|
@ -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):
|
if is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
|
||||||
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
|
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
|
||||||
manage=manage, task_id=task_id)
|
manage=manage, task_id=task_id)
|
||||||
proc = run(command, async=True)
|
proc = run(command, run_async=True)
|
||||||
yield proc
|
yield proc
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ def fire_pending_messages(settings, db):
|
||||||
|
|
||||||
if has_pending_messages(settings, db):
|
if has_pending_messages(settings, db):
|
||||||
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
|
||||||
proc = run(command, async=True)
|
proc = run(command, run_async=True)
|
||||||
yield proc
|
yield proc
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ SECRET_KEY = '{{ secret_key }}'
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ INSTALLED_APPS = [
|
||||||
'admin_tools.dashboard',
|
'admin_tools.dashboard',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
|
'django_filters',
|
||||||
'passlib.ext.django',
|
'passlib.ext.django',
|
||||||
'django_countries',
|
'django_countries',
|
||||||
# 'debug_toolbar',
|
# 'debug_toolbar',
|
||||||
|
@ -84,6 +86,21 @@ INSTALLED_APPS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
|
||||||
|
'orchestra.core.caches.RequestCacheMiddleware',
|
||||||
|
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
|
||||||
|
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
ROOT_URLCONF = '{{ project_name }}.urls'
|
ROOT_URLCONF = '{{ project_name }}.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
@ -127,6 +144,24 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
|
||||||
|
|
||||||
|
@ -168,22 +203,6 @@ LOCALE_PATHS = (
|
||||||
ORCHESTRA_SITE_NAME = '{{ project_name }}'
|
ORCHESTRA_SITE_NAME = '{{ project_name }}'
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
# 'django.middleware.locale.LocaleMiddleware'
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'orchestra.core.caches.RequestCacheMiddleware',
|
|
||||||
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
|
|
||||||
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'accounts.Account'
|
AUTH_USER_MODEL = 'accounts.Account'
|
||||||
|
|
||||||
|
|
||||||
|
@ -242,7 +261,6 @@ PASSLIB_CONFIG = (
|
||||||
"default = sha512_crypt\n"
|
"default = sha512_crypt\n"
|
||||||
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
|
||||||
" django_des_crypt, des_crypt, hex_md5\n"
|
" django_des_crypt, des_crypt, hex_md5\n"
|
||||||
"all__vary_rounds = 0.05\n"
|
|
||||||
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
"django_pbkdf2_sha256__min_rounds = 10000\n"
|
||||||
"sha512_crypt__min_rounds = 80000\n"
|
"sha512_crypt__min_rounds = 80000\n"
|
||||||
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"
|
||||||
|
|
|
@ -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 import helpers
|
||||||
from django.contrib.admin.utils import NestedObjects, quote
|
from django.contrib.admin.utils import NestedObjects, quote
|
||||||
from django.contrib.auth import get_permission_codename
|
from django.contrib.auth import get_permission_codename
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.urls import reverse, NoReverseMatch
|
||||||
from django.db import router
|
from django.db import router
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
@ -175,7 +175,7 @@ def delete_related_services(modeladmin, request, queryset):
|
||||||
for model, objs in collector.model_objs.items():
|
for model, objs in collector.model_objs.items():
|
||||||
count = 0
|
count = 0
|
||||||
# discount main systemuser
|
# discount main systemuser
|
||||||
if model is modeladmin.model.main_systemuser.field.rel.to:
|
if model is modeladmin.model.main_systemuser.field.related_model:
|
||||||
count = len(objs) - 1
|
count = len(objs) - 1
|
||||||
# Discount account
|
# Discount account
|
||||||
elif model is not modeladmin.model and model in registered_services:
|
elif model is not modeladmin.model and model in registered_services:
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.conf.urls import url
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.contrib.auth import admin as auth
|
from django.contrib.auth import admin as auth
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -158,6 +158,7 @@ class AccountListAdmin(AccountAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
change_list_template = 'admin/accounts/account/select_account_list.html'
|
change_list_template = 'admin/accounts/account/select_account_list.html'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def select_account(self, instance):
|
def select_account(self, instance):
|
||||||
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
||||||
context = {
|
context = {
|
||||||
|
@ -167,7 +168,6 @@ class AccountListAdmin(AccountAdmin):
|
||||||
}
|
}
|
||||||
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
|
||||||
select_account.short_description = _("account")
|
select_account.short_description = _("account")
|
||||||
select_account.allow_tags = True
|
|
||||||
select_account.admin_order_field = 'username'
|
select_account.admin_order_field = 'username'
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
@ -207,6 +207,7 @@ class AccountAdminMixin(object):
|
||||||
account = None
|
account = None
|
||||||
list_select_related = ('account',)
|
list_select_related = ('account',)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_active(self, instance):
|
def display_active(self, instance):
|
||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
||||||
|
@ -215,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 style="width:13px" src="%s" alt="False" title="%s">' % (static('admin/img/inline-delete.svg'), msg)
|
||||||
return '<img src="%s" alt="False">' % static('admin/img/icon-yes.svg')
|
return '<img src="%s" alt="False">' % static('admin/img/icon-yes.svg')
|
||||||
display_active.short_description = _("active")
|
display_active.short_description = _("active")
|
||||||
display_active.allow_tags = True
|
|
||||||
display_active.admin_order_field = 'is_active'
|
display_active.admin_order_field = 'is_active'
|
||||||
|
|
||||||
def account_link(self, instance):
|
def account_link(self, instance):
|
||||||
account = instance.account if instance.pk else self.account
|
account = instance.account if instance.pk else self.account
|
||||||
return admin_link()(account)
|
return admin_link()(account)
|
||||||
account_link.short_description = _("account")
|
account_link.short_description = _("account")
|
||||||
account_link.allow_tags = True
|
|
||||||
account_link.admin_order_field = 'account__username'
|
account_link.admin_order_field = 'account__username'
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
|
|
@ -47,7 +47,7 @@ def create_account_creation_form():
|
||||||
# Previous validation error
|
# Previous validation error
|
||||||
return
|
return
|
||||||
errors = {}
|
errors = {}
|
||||||
systemuser_model = Account.main_systemuser.field.rel.to
|
systemuser_model = Account.main_systemuser.field.related_model
|
||||||
if systemuser_model.objects.filter(username=account.username).exists():
|
if systemuser_model.objects.filter(username=account.username).exists():
|
||||||
errors['username'] = _("A system user with this name already exists.")
|
errors['username'] = _("A system user with this name already exists.")
|
||||||
for model, key, related_kwargs, __ in create_related:
|
for model, key, related_kwargs, __ in create_related:
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ class Migration(migrations.Migration):
|
||||||
('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')),
|
('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')),
|
||||||
('is_active', models.BooleanField(help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')),
|
('is_active', models.BooleanField(help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')),
|
||||||
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
|
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
|
||||||
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, related_name='accounts_main')),
|
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:08
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import orchestra.contrib.accounts.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('accounts', '0001_initial'), ('accounts', '0002_auto_20170528_2005'), ('accounts', '0003_auto_20210330_1049'), ('accounts', '0004_auto_20210422_1108')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('systemusers', '0001_initial'),
|
||||||
|
('auth', '0006_require_contenttypes_0002'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Account',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('username', models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username')),
|
||||||
|
('short_name', models.CharField(blank=True, max_length=64, verbose_name='short name')),
|
||||||
|
('full_name', models.CharField(max_length=256, verbose_name='full name')),
|
||||||
|
('email', models.EmailField(help_text='Used for password recovery', max_length=254, verbose_name='email address')),
|
||||||
|
('type', models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type')),
|
||||||
|
('language', models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language')),
|
||||||
|
('comments', models.TextField(blank=True, max_length=256, verbose_name='comments')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('main_systemuser', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main', to='systemusers.SystemUser')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='account',
|
||||||
|
managers=[
|
||||||
|
('objects', orchestra.contrib.accounts.models.AccountManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='main_systemuser',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts_main', to='systemusers.SystemUser'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_auto_20170528_2005'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -29,7 +29,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
|
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
|
||||||
])
|
])
|
||||||
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
|
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
|
||||||
related_name='accounts_main', editable=False)
|
related_name='accounts_main', editable=False, on_delete=models.SET_NULL)
|
||||||
short_name = models.CharField(_("short name"), max_length=64, blank=True)
|
short_name = models.CharField(_("short name"), max_length=64, blank=True)
|
||||||
full_name = models.CharField(_("full name"), max_length=256)
|
full_name = models.CharField(_("full name"), max_length=256)
|
||||||
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
||||||
|
@ -53,6 +53,7 @@ class Account(auth.AbstractBaseUser):
|
||||||
REQUIRED_FIELDS = ['email']
|
REQUIRED_FIELDS = ['email']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
# ignore `is_staff` kwarg because is handled with `is_superuser`
|
||||||
kwargs.pop('is_staff', None)
|
kwargs.pop('is_staff', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from datetime import date
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.forms.models import modelformset_factory
|
from django.forms.models import modelformset_factory
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
|
|
@ -2,11 +2,12 @@ from django import forms
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Sum, Prefetch
|
from django.db.models import F, Sum, Prefetch
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -15,7 +16,7 @@ from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
|
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from . import settings, actions
|
from . import settings, actions
|
||||||
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
|
||||||
|
@ -67,6 +68,7 @@ class BillLineInline(admin.TabularInline):
|
||||||
|
|
||||||
order_link = admin_link('order', display='pk')
|
order_link = admin_link('order', display='pk')
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_total(self, line):
|
def display_total(self, line):
|
||||||
if line.pk:
|
if line.pk:
|
||||||
total = line.compute_total()
|
total = line.compute_total()
|
||||||
|
@ -78,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" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
|
||||||
return '<a href="%s">%s</a>' % (url, total)
|
return '<a href="%s">%s</a>' % (url, total)
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.allow_tags = True
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
|
@ -104,27 +105,26 @@ class ClosedBillLineInline(BillLineInline):
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
can_delete = False
|
can_delete = False
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_description(self, line):
|
def display_description(self, line):
|
||||||
descriptions = [line.description]
|
descriptions = [line.description]
|
||||||
for subline in line.sublines.all():
|
for subline in line.sublines.all():
|
||||||
descriptions.append(' '*4+subline.description)
|
descriptions.append(' ' * 4 + subline.description)
|
||||||
return '<br>'.join(descriptions)
|
return '<br>'.join(descriptions)
|
||||||
display_description.short_description = _("Description")
|
display_description.short_description = _("Description")
|
||||||
display_description.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_subtotal(self, line):
|
def display_subtotal(self, line):
|
||||||
subtotals = [' ' + str(line.subtotal)]
|
subtotals = [' ' + str(line.subtotal)]
|
||||||
for subline in line.sublines.all():
|
for subline in line.sublines.all():
|
||||||
subtotals.append(str(subline.total))
|
subtotals.append(str(subline.total))
|
||||||
return '<br>'.join(subtotals)
|
return '<br>'.join(subtotals)
|
||||||
display_subtotal.short_description = _("Subtotal")
|
display_subtotal.short_description = _("Subtotal")
|
||||||
display_subtotal.allow_tags = True
|
|
||||||
|
|
||||||
def display_total(self, line):
|
def display_total(self, line):
|
||||||
if line.pk:
|
if line.pk:
|
||||||
return line.compute_total()
|
return line.compute_total()
|
||||||
display_total.short_description = _("Total")
|
display_total.short_description = _("Total")
|
||||||
display_total.allow_tags = True
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
@ -242,6 +242,7 @@ class BillLineManagerAdmin(BillLineAdmin):
|
||||||
|
|
||||||
|
|
||||||
class BillAdminMixin(AccountAdminMixin):
|
class BillAdminMixin(AccountAdminMixin):
|
||||||
|
@mark_safe
|
||||||
def display_total_with_subtotals(self, bill):
|
def display_total_with_subtotals(self, bill):
|
||||||
if bill.pk:
|
if bill.pk:
|
||||||
currency = settings.BILLS_CURRENCY.lower()
|
currency = settings.BILLS_CURRENCY.lower()
|
||||||
|
@ -251,10 +252,10 @@ class BillAdminMixin(AccountAdminMixin):
|
||||||
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
||||||
subtotals = '\n'.join(subtotals)
|
subtotals = '\n'.join(subtotals)
|
||||||
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
|
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
|
||||||
display_total_with_subtotals.allow_tags = True
|
|
||||||
display_total_with_subtotals.short_description = _("total")
|
display_total_with_subtotals.short_description = _("total")
|
||||||
display_total_with_subtotals.admin_order_field = 'approx_total'
|
display_total_with_subtotals.admin_order_field = 'approx_total'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_payment_state(self, bill):
|
def display_payment_state(self, bill):
|
||||||
if bill.pk:
|
if bill.pk:
|
||||||
t_opts = bill.transactions.model._meta
|
t_opts = bill.transactions.model._meta
|
||||||
|
@ -276,7 +277,6 @@ class BillAdminMixin(AccountAdminMixin):
|
||||||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||||
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
||||||
url=url, color=color, name=state, title=title)
|
url=url, color=color, name=state, title=title)
|
||||||
display_payment_state.allow_tags = True
|
|
||||||
display_payment_state.short_description = _("Payment")
|
display_payment_state.short_description = _("Payment")
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
@ -376,16 +376,14 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
|
||||||
|
|
||||||
def display_total(self, bill):
|
def display_total(self, bill):
|
||||||
currency = settings.BILLS_CURRENCY.lower()
|
currency = settings.BILLS_CURRENCY.lower()
|
||||||
return '%s &%s;' % (bill.compute_total(), currency)
|
return format_html('{} &{};', bill.compute_total(), currency)
|
||||||
display_total.allow_tags = True
|
|
||||||
display_total.short_description = _("total")
|
display_total.short_description = _("total")
|
||||||
display_total.admin_order_field = 'approx_total'
|
display_total.admin_order_field = 'approx_total'
|
||||||
|
|
||||||
def type_link(self, bill):
|
def type_link(self, bill):
|
||||||
bill_type = bill.type.lower()
|
bill_type = bill.type.lower()
|
||||||
url = reverse('admin:bills_%s_changelist' % bill_type)
|
url = reverse('admin:bills_%s_changelist' % bill_type)
|
||||||
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
|
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
|
||||||
type_link.allow_tags = True
|
|
||||||
type_link.short_description = _("type")
|
type_link.short_description = _("type")
|
||||||
type_link.admin_order_field = 'type'
|
type_link.admin_order_field = 'type'
|
||||||
|
|
||||||
|
@ -479,7 +477,7 @@ class BillContactInline(admin.StackedInline):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||||
return super().formfield_for_dbfield(db_field, **kwargs)
|
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from orchestra.api import router, LogApiMixin
|
from orchestra.api import router, LogApiMixin
|
||||||
from orchestra.contrib.accounts.api import AccountApiMixin
|
from orchestra.contrib.accounts.api import AccountApiMixin
|
||||||
|
@ -15,7 +15,7 @@ class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Bill.objects.all()
|
queryset = Bill.objects.all()
|
||||||
serializer_class = BillSerializer
|
serializer_class = BillSerializer
|
||||||
|
|
||||||
@detail_route(methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def document(self, request, pk):
|
def document(self, request, pk):
|
||||||
bill = self.get_object()
|
bill = self.get_object()
|
||||||
content_type = request.META.get('HTTP_ACCEPT')
|
content_type = request.META.get('HTTP_ACCEPT')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib.admin import SimpleListFilter
|
from django.contrib.admin import SimpleListFilter
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -21,7 +21,7 @@ def validate_contact(request, bill, error=True):
|
||||||
message = msg.format(relation=_("Related"), account=account, url=url)
|
message = msg.format(relation=_("Related"), account=account, url=url)
|
||||||
send(request, mark_safe(message))
|
send(request, mark_safe(message))
|
||||||
valid = False
|
valid = False
|
||||||
main = type(bill).account.field.rel.to.objects.get_main()
|
main = type(bill).account.field.related_model.objects.get_main()
|
||||||
if not hasattr(main, 'billcontact'):
|
if not hasattr(main, 'billcontact'):
|
||||||
account = force_text(main)
|
account = force_text(main)
|
||||||
url = reverse('admin:accounts_account_change', args=(main.id,))
|
url = reverse('admin:accounts_account_change', args=(main.id,))
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bill',
|
model_name='bill',
|
||||||
name='amend_of',
|
name='amend_of',
|
||||||
field=models.ForeignKey(to='bills.Bill', blank=True, related_name='amends', verbose_name='amend of', null=True),
|
field=models.ForeignKey(to='bills.Bill', blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='amends', verbose_name='amend of', null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='billcontact',
|
model_name='billcontact',
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,12 @@
|
||||||
import datetime
|
import datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.validators import ValidationError, RegexValidator
|
from django.core.validators import ValidationError, RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Sum
|
from django.db.models import F, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.template import loader, Context
|
from django.template import loader
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
@ -24,7 +24,7 @@ from . import settings
|
||||||
|
|
||||||
class BillContact(models.Model):
|
class BillContact(models.Model):
|
||||||
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
|
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='billcontact')
|
related_name='billcontact', on_delete=models.CASCADE)
|
||||||
name = models.CharField(_("name"), max_length=256, blank=True,
|
name = models.CharField(_("name"), max_length=256, blank=True,
|
||||||
help_text=_("Account full name will be used when left blank."))
|
help_text=_("Account full name will be used when left blank."))
|
||||||
address = models.TextField(_("address"))
|
address = models.TextField(_("address"))
|
||||||
|
@ -102,9 +102,9 @@ class Bill(models.Model):
|
||||||
|
|
||||||
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='%(class)s')
|
related_name='%(class)s', on_delete=models.CASCADE)
|
||||||
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
|
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
|
||||||
related_name='amends')
|
related_name='amends', on_delete=models.SET_NULL)
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||||
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
||||||
|
@ -303,7 +303,7 @@ class Bill(models.Model):
|
||||||
with translation.override(language or self.account.language):
|
with translation.override(language or self.account.language):
|
||||||
if payment is False:
|
if payment is False:
|
||||||
payment = self.account.paymentsources.get_default()
|
payment = self.account.paymentsources.get_default()
|
||||||
context = Context({
|
context = {
|
||||||
'bill': self,
|
'bill': self,
|
||||||
'lines': self.lines.all().prefetch_related('sublines'),
|
'lines': self.lines.all().prefetch_related('sublines'),
|
||||||
'seller': self.seller,
|
'seller': self.seller,
|
||||||
|
@ -318,7 +318,7 @@ class Bill(models.Model):
|
||||||
'payment': payment and payment.get_bill_context(),
|
'payment': payment and payment.get_bill_context(),
|
||||||
'default_due_date': self.get_due_date(payment=payment),
|
'default_due_date': self.get_due_date(payment=payment),
|
||||||
'now': timezone.now(),
|
'now': timezone.now(),
|
||||||
})
|
}
|
||||||
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
|
||||||
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
|
||||||
bill_template = loader.get_template(template)
|
bill_template = loader.get_template(template)
|
||||||
|
@ -416,7 +416,7 @@ class ProForma(Bill):
|
||||||
|
|
||||||
class BillLine(models.Model):
|
class BillLine(models.Model):
|
||||||
""" Base model for bill item representation """
|
""" Base model for bill item representation """
|
||||||
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
|
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines', on_delete=models.CASCADE)
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
||||||
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
|
||||||
|
@ -434,7 +434,7 @@ class BillLine(models.Model):
|
||||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||||
# Amendment
|
# Amendment
|
||||||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||||
related_name='amendment_lines', null=True, blank=True)
|
related_name='amendment_lines', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'id'
|
get_latest_by = 'id'
|
||||||
|
@ -495,7 +495,7 @@ class BillSubline(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: order info for undoing
|
# TODO: order info for undoing
|
||||||
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
|
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines', on_delete=models.CASCADE)
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
total = models.DecimalField(max_digits=12, decimal_places=2)
|
total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from orchestra.admin.actions import SendEmail
|
||||||
from orchestra.admin.utils import insertattr, change_url
|
from orchestra.admin.utils import insertattr, change_url
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin
|
from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin
|
||||||
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from .filters import EmailUsageListFilter
|
from .filters import EmailUsageListFilter
|
||||||
from .models import Contact
|
from .models import Contact
|
||||||
|
@ -72,7 +72,7 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(130)
|
||||||
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ class ContactInline(admin.StackedInline):
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
if db_field.name == 'email_usage':
|
if db_field.name == 'email_usage':
|
||||||
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
|
||||||
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -33,7 +33,7 @@ class Contact(models.Model):
|
||||||
objects = ContactQuerySet.as_manager()
|
objects = ContactQuerySet.as_manager()
|
||||||
|
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='contacts', null=True)
|
related_name='contacts', null=True, on_delete=models.SET_NULL)
|
||||||
short_name = models.CharField(_("short name"), max_length=128)
|
short_name = models.CharField(_("short name"), max_length=128)
|
||||||
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
full_name = models.CharField(_("full name"), max_length=256, blank=True)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
|
@ -50,14 +52,14 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_prefetch_related = ('users',)
|
list_prefetch_related = ('users',)
|
||||||
actions = (list_accounts, save_selected)
|
actions = (list_accounts, save_selected)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_users(self, db):
|
def display_users(self, db):
|
||||||
links = []
|
links = []
|
||||||
for user in db.users.all():
|
for user in db.users.all():
|
||||||
link = '<a href="%s">%s</a>' % (change_url(user), user.username)
|
link = format_html('<a href="{}">{}</a>', change_url(user), user.username)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
display_users.short_description = _("Users")
|
display_users.short_description = _("Users")
|
||||||
display_users.allow_tags = True
|
|
||||||
display_users.admin_order_field = 'users__username'
|
display_users.admin_order_field = 'users__username'
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
|
@ -99,14 +101,14 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
|
||||||
list_prefetch_related = ('databases',)
|
list_prefetch_related = ('databases',)
|
||||||
actions = (list_accounts, save_selected)
|
actions = (list_accounts, save_selected)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_databases(self, user):
|
def display_databases(self, user):
|
||||||
links = []
|
links = []
|
||||||
for db in user.databases.all():
|
for db in user.databases.all():
|
||||||
link = '<a href="%s">%s</a>' % (change_url(db), db.name)
|
link = format_html('<a href="{}">{}</a>', change_url(db), db.name)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
display_databases.short_description = _("Databases")
|
display_databases.short_description = _("Databases")
|
||||||
display_databases.allow_tags = True
|
|
||||||
display_databases.admin_order_field = 'databases__name'
|
display_databases.admin_order_field = 'databases__name'
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
|
|
|
@ -91,7 +91,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
|
||||||
|
|
||||||
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
||||||
class ReadOnlyPasswordHashWidget(forms.Widget):
|
class ReadOnlyPasswordHashWidget(forms.Widget):
|
||||||
def render(self, name, value, attrs):
|
def render(self, name, value, attrs, renderer=None):
|
||||||
original = ReadOnlyPasswordHashField.widget().render(name, value, attrs)
|
original = ReadOnlyPasswordHashField.widget().render(name, value, attrs)
|
||||||
if 'Invalid' not in original:
|
if 'Invalid' not in original:
|
||||||
return original
|
return original
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||||
('name', models.CharField(verbose_name='name', max_length=64, validators=[orchestra.core.validators.validate_name])),
|
('name', models.CharField(verbose_name='name', max_length=64, validators=[orchestra.core.validators.validate_name])),
|
||||||
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
||||||
('account', models.ForeignKey(related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -29,7 +30,7 @@ class Migration(migrations.Migration):
|
||||||
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
|
||||||
('password', models.CharField(verbose_name='password', max_length=256)),
|
('password', models.CharField(verbose_name='password', max_length=256)),
|
||||||
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
|
||||||
('account', models.ForeignKey(related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'DB users',
|
'verbose_name_plural': 'DB users',
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('databases', '0001_initial'), ('databases', '0002_auto_20170528_2005'), ('databases', '0003_database_comments'), ('databases', '0004_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Database',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
|
||||||
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DatabaseUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('username', models.CharField(max_length=16, validators=[orchestra.core.validators.validate_name], verbose_name='username')),
|
||||||
|
('password', models.CharField(max_length=256, verbose_name='password')),
|
||||||
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'DB users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='database',
|
||||||
|
name='users',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='databases', to='databases.DatabaseUser', verbose_name='users'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='databaseuser',
|
||||||
|
unique_together=set([('username', 'type')]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='database',
|
||||||
|
unique_together=set([('name', 'type')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='database',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('databases', '0003_database_comments'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='database',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='databaseuser',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,8 +20,8 @@ class Database(models.Model):
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
default=settings.DATABASES_DEFAULT_TYPE)
|
default=settings.DATABASES_DEFAULT_TYPE)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='databases')
|
verbose_name=_("Account"), related_name='databases')
|
||||||
comments = models.TextField(default="", blank=True)
|
comments = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -60,8 +60,8 @@ class DatabaseUser(models.Model):
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
default=settings.DATABASES_DEFAULT_TYPE)
|
default=settings.DATABASES_DEFAULT_TYPE)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='databaseusers')
|
verbose_name=_("Account"), related_name='databaseusers')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("DB users")
|
verbose_name_plural = _("DB users")
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
from unittest import skip
|
import MySQLdb
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
|
||||||
|
|
||||||
from orchestra.admin.utils import change_url
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Route, Server
|
||||||
from orchestra.utils.sys import sshrun
|
from orchestra.utils.sys import sshrun
|
||||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error,
|
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||||
snapshot_on_error)
|
save_response_on_error, snapshot_on_error)
|
||||||
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from ... import backends, settings
|
from ... import backends, settings
|
||||||
from ...models import Database, DatabaseUser
|
from ...models import Database, DatabaseUser
|
||||||
|
|
||||||
|
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||||
|
|
||||||
|
|
||||||
class DatabaseTestMixin(object):
|
class DatabaseTestMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
|
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
|
||||||
|
@ -51,7 +53,6 @@ class DatabaseTestMixin(object):
|
||||||
def add_group(self, username, groupname):
|
def add_group(self, username, groupname):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -59,7 +60,6 @@ class DatabaseTestMixin(object):
|
||||||
self.add(dbname, username, password)
|
self.add(dbname, username, password)
|
||||||
self.validate_create_table(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):
|
def test_delete(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -71,7 +71,6 @@ class DatabaseTestMixin(object):
|
||||||
self.validate_delete(dbname, username, password)
|
self.validate_delete(dbname, username, password)
|
||||||
self.validate_delete_user(dbname, username)
|
self.validate_delete_user(dbname, username)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_change_password(self):
|
def test_change_password(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -85,7 +84,6 @@ class DatabaseTestMixin(object):
|
||||||
self.validate_login_error(dbname, username, password)
|
self.validate_login_error(dbname, username, password)
|
||||||
self.validate_create_table(dbname, username, new_password)
|
self.validate_create_table(dbname, username, new_password)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_add_user(self):
|
def test_add_user(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -103,7 +101,6 @@ class DatabaseTestMixin(object):
|
||||||
self.validate_create_table(dbname, username, password)
|
self.validate_create_table(dbname, username, password)
|
||||||
self.validate_create_table(dbname, username2, password2)
|
self.validate_create_table(dbname, username2, password2)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_delete_user(self):
|
def test_delete_user(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -123,7 +120,6 @@ class DatabaseTestMixin(object):
|
||||||
self.validate_login_error(dbname, username2, password2)
|
self.validate_login_error(dbname, username2, password2)
|
||||||
self.validate_delete_user(username2, password2)
|
self.validate_delete_user(username2, password2)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_swap_user(self):
|
def test_swap_user(self):
|
||||||
dbname = '%s_database' % random_ascii(5)
|
dbname = '%s_database' % random_ascii(5)
|
||||||
username = '%s_dbuser' % random_ascii(5)
|
username = '%s_dbuser' % random_ascii(5)
|
||||||
|
@ -187,6 +183,7 @@ class MySQLControllerMixin(object):
|
||||||
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
|
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||||
class RESTDatabaseMixin(DatabaseTestMixin):
|
class RESTDatabaseMixin(DatabaseTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTDatabaseMixin, self).setUp()
|
super(RESTDatabaseMixin, self).setUp()
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
|
@ -72,9 +74,8 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
def structured_name(self, domain):
|
def structured_name(self, domain):
|
||||||
if domain.is_top:
|
if domain.is_top:
|
||||||
return domain.name
|
return domain.name
|
||||||
return ' '*4 + domain.name
|
return mark_safe(' '*4 + domain.name)
|
||||||
structured_name.short_description = _("name")
|
structured_name.short_description = _("name")
|
||||||
structured_name.allow_tags = True
|
|
||||||
structured_name.admin_order_field = 'structured_name'
|
structured_name.admin_order_field = 'structured_name'
|
||||||
|
|
||||||
def display_is_top(self, domain):
|
def display_is_top(self, domain):
|
||||||
|
@ -83,6 +84,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
display_is_top.boolean = True
|
display_is_top.boolean = True
|
||||||
display_is_top.admin_order_field = 'top'
|
display_is_top.admin_order_field = 'top'
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_websites(self, domain):
|
def display_websites(self, domain):
|
||||||
if apps.isinstalled('orchestra.contrib.websites'):
|
if apps.isinstalled('orchestra.contrib.websites'):
|
||||||
websites = domain.websites.all()
|
websites = domain.websites.all()
|
||||||
|
@ -92,22 +94,22 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
site_link = get_on_site_link(website.get_absolute_url())
|
site_link = get_on_site_link(website.get_absolute_url())
|
||||||
admin_url = change_url(website)
|
admin_url = change_url(website)
|
||||||
title = _("Edit website")
|
title = _("Edit website")
|
||||||
link = '<a href="%s" title="%s">%s %s</a>' % (
|
link = format_html('<a href="{}" title="{}">{} {}</a>',
|
||||||
admin_url, title, website.name, site_link)
|
admin_url, title, website.name, site_link)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return '<br>'.join(links)
|
return '<br>'.join(links)
|
||||||
add_url = reverse('admin:websites_website_add')
|
add_url = reverse('admin:websites_website_add')
|
||||||
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
|
||||||
image = '<img src="%s"></img>' % static('orchestra/images/add.png')
|
add_link = format_html(
|
||||||
add_link = '<a href="%s" title="%s">%s</a>' % (
|
'<a href="{}" title="{}"><img src="{}" /></a>', add_url,
|
||||||
add_url, _("Add website"), image
|
_("Add website"), static('orchestra/images/add.png'),
|
||||||
)
|
)
|
||||||
return _("No website %s") % (add_link)
|
return _("No website %s") % (add_link)
|
||||||
return '---'
|
return '---'
|
||||||
display_websites.admin_order_field = 'websites__name'
|
display_websites.admin_order_field = 'websites__name'
|
||||||
display_websites.short_description = _("Websites")
|
display_websites.short_description = _("Websites")
|
||||||
display_websites.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_addresses(self, domain):
|
def display_addresses(self, domain):
|
||||||
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
if apps.isinstalled('orchestra.contrib.mailboxes'):
|
||||||
add_url = reverse('admin:mailboxes_address_add')
|
add_url = reverse('admin:mailboxes_address_add')
|
||||||
|
@ -126,10 +128,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
return '---'
|
return '---'
|
||||||
display_addresses.short_description = _("Addresses")
|
display_addresses.short_description = _("Addresses")
|
||||||
display_addresses.admin_order_field = 'addresses__count'
|
display_addresses.admin_order_field = 'addresses__count'
|
||||||
display_addresses.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def implicit_records(self, domain):
|
def implicit_records(self, domain):
|
||||||
defaults = []
|
|
||||||
types = set(domain.records.values_list('type', flat=True))
|
types = set(domain.records.values_list('type', flat=True))
|
||||||
ttl = settings.DOMAINS_DEFAULT_TTL
|
ttl = settings.DOMAINS_DEFAULT_TTL
|
||||||
lines = []
|
lines = []
|
||||||
|
@ -141,14 +142,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
value=record.value
|
value=record.value
|
||||||
)
|
)
|
||||||
if not domain.record_is_implicit(record, types):
|
if not domain.record_is_implicit(record, types):
|
||||||
line = '<strike>%s</strike>' % line
|
line = format_html('<strike>{}</strike>', line)
|
||||||
if record.type is Record.SOA:
|
if record.type is Record.SOA:
|
||||||
lines.insert(0, line)
|
lines.insert(0, line)
|
||||||
else:
|
else:
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return '<br>'.join(lines)
|
return '<br>'.join(lines)
|
||||||
implicit_records.short_description = _("Implicit records")
|
implicit_records.short_description = _("Implicit records")
|
||||||
implicit_records.allow_tags = True
|
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
""" Add SOA fields when domain is top """
|
""" Add SOA fields when domain is top """
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from orchestra.api import router
|
from orchestra.api import router
|
||||||
|
@ -19,7 +19,7 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
||||||
qs = super(DomainViewSet, self).get_queryset()
|
qs = super(DomainViewSet, self).get_queryset()
|
||||||
return qs.prefetch_related('records')
|
return qs.prefetch_related('records')
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def view_zone(self, request, pk=None):
|
def view_zone(self, request, pk=None):
|
||||||
domain = self.get_object()
|
domain = self.get_object()
|
||||||
return Response({
|
return Response({
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.domains.utils
|
import orchestra.contrib.domains.utils
|
||||||
import orchestra.contrib.domains.validators
|
import orchestra.contrib.domains.validators
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,8 +21,8 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
('name', models.CharField(unique=True, max_length=256, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name', help_text='Domain or subdomain name.')),
|
('name', models.CharField(unique=True, max_length=256, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name', help_text='Domain or subdomain name.')),
|
||||||
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, verbose_name='serial', help_text='Serial number')),
|
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, verbose_name='serial', help_text='Serial number')),
|
||||||
('account', models.ForeignKey(related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
|
||||||
('top', models.ForeignKey(null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
('top', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -31,7 +32,7 @@ class Migration(migrations.Migration):
|
||||||
('ttl', models.CharField(help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL', blank=True)),
|
('ttl', models.CharField(help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL', blank=True)),
|
||||||
('type', models.CharField(max_length=32, verbose_name='type', choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SOA', 'SOA')])),
|
('type', models.CharField(max_length=32, verbose_name='type', choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SOA', 'SOA')])),
|
||||||
('value', models.CharField(max_length=256, verbose_name='value')),
|
('value', models.CharField(max_length=256, verbose_name='value')),
|
||||||
('domain', models.ForeignKey(related_name='records', to='domains.Domain', verbose_name='domain')),
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.contrib.domains.utils
|
||||||
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('domains', '0001_initial'), ('domains', '0002_auto_20150715_1017'), ('domains', '0003_auto_20150720_1121'), ('domains', '0004_auto_20150720_1121'), ('domains', '0005_auto_20160219_1034'), ('domains', '0006_auto_20170528_2011'), ('domains', '0007_auto_20190805_1134'), ('domains', '0008_domain_dns2136_address_match_list'), ('domains', '0009_auto_20200204_1217'), ('domains', '0010_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Domain',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name')),
|
||||||
|
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')),
|
||||||
|
('account', models.ForeignKey(blank=True, help_text='Automatically selected for subdomains.', on_delete=django.db.models.deletion.CASCADE, related_name='domains', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('top', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Record',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ttl', models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL')),
|
||||||
|
('type', models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF')], max_length=32, verbose_name='type')),
|
||||||
|
('value', models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value')),
|
||||||
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='serial',
|
||||||
|
field=models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, editable=False, help_text='A revision number that changes whenever this domain is updated.', verbose_name='serial'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='expire',
|
||||||
|
field=models.CharField(blank=True, help_text='The time that a secondary server will keep trying to complete a zone transfer. If this time expires prior to a successful zone transfer, the secondary server will expire its zone file. This means the secondary will stop answering queries. The default value is <tt>4w</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='expire'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='min_ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='refresh',
|
||||||
|
field=models.CharField(blank=True, help_text="The time a secondary DNS server waits before querying the primary DNS server's SOA record to check for changes. When the refresh time expires, the secondary DNS server requests a copy of the current SOA record from the primary. The primary DNS server complies with this request. The secondary DNS server compares the serial number of the primary DNS server's current SOA record and the serial number in it's own SOA record. If they are different, the secondary DNS server will request a zone transfer from the primary DNS server. The default value is <tt>1d</tt>.", max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='refresh'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='retry',
|
||||||
|
field=models.CharField(blank=True, help_text='The time a secondary server waits before retrying a failed zone transfer. Normally, the retry time is less than the refresh time. The default value is <tt>2h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='retry'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='top',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain', verbose_name='top domain'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='domain',
|
||||||
|
name='dns2136_address_match_list',
|
||||||
|
field=models.CharField(blank=True, default='key pangea.key;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.domains.validators
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='domain',
|
model_name='domain',
|
||||||
name='top',
|
name='top',
|
||||||
field=models.ForeignKey(editable=False, verbose_name='top domain', related_name='subdomain_set', to='domains.Domain', null=True),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, editable=False, verbose_name='top domain', related_name='subdomain_set', to='domains.Domain', null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='record',
|
model_name='record',
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import orchestra.contrib.domains.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0009_auto_20200204_1217'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='domain',
|
||||||
|
name='min_ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='record',
|
||||||
|
name='ttl',
|
||||||
|
field=models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -31,9 +31,9 @@ class Domain(models.Model):
|
||||||
validators.validate_allowed_domain
|
validators.validate_allowed_domain
|
||||||
])
|
])
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
|
||||||
related_name='domains', help_text=_("Automatically selected for subdomains."))
|
related_name='domains', on_delete=models.CASCADE, help_text=_("Automatically selected for subdomains."))
|
||||||
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
|
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
|
||||||
editable=False, verbose_name=_("top domain"))
|
editable=False, verbose_name=_("top domain"), on_delete=models.CASCADE)
|
||||||
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
|
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
|
||||||
help_text=_("A revision number that changes whenever this domain is updated."))
|
help_text=_("A revision number that changes whenever this domain is updated."))
|
||||||
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
|
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
|
||||||
|
@ -318,7 +318,7 @@ class Record(models.Model):
|
||||||
SOA: (validators.validate_soa_record,),
|
SOA: (validators.validate_soa_record,),
|
||||||
}
|
}
|
||||||
|
|
||||||
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
|
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE)
|
||||||
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
||||||
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
||||||
validators=[validators.validate_zone_interval])
|
validators=[validators.validate_zone_interval])
|
||||||
|
|
|
@ -2,10 +2,9 @@ import os
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from unittest import skip
|
|
||||||
|
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Server, Route
|
||||||
|
@ -21,9 +20,9 @@ run = partial(run, display=False)
|
||||||
|
|
||||||
class DomainTestMixin(object):
|
class DomainTestMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_MASTER_SERVER', 'localhost')
|
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)
|
MASTER_SERVER_ADDR = socket.gethostbyname(MASTER_SERVER)
|
||||||
SLAVE_SERVER_ADDR = '127.0.0.2'
|
SLAVE_SERVER_ADDR = socket.gethostbyname(SLAVE_SERVER)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
djsettings.DEBUG = True
|
djsettings.DEBUG = True
|
||||||
|
@ -177,7 +176,6 @@ class DomainTestMixin(object):
|
||||||
self.assertEqual('CNAME', cname[3])
|
self.assertEqual('CNAME', cname[3])
|
||||||
self.assertEqual('external.server.org.', cname[4])
|
self.assertEqual('external.server.org.', cname[4])
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -187,7 +185,6 @@ class DomainTestMixin(object):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.validate_add(self.SLAVE_SERVER_ADDR, self.domain_name)
|
self.validate_add(self.SLAVE_SERVER_ADDR, self.domain_name)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -197,7 +194,6 @@ class DomainTestMixin(object):
|
||||||
self.validate_delete(self.MASTER_SERVER_ADDR, name)
|
self.validate_delete(self.MASTER_SERVER_ADDR, name)
|
||||||
self.validate_delete(self.SLAVE_SERVER_ADDR, name)
|
self.validate_delete(self.SLAVE_SERVER_ADDR, name)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -214,7 +210,6 @@ class DomainTestMixin(object):
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
self.validate_www_update(self.SLAVE_SERVER_ADDR, self.domain_name)
|
self.validate_www_update(self.SLAVE_SERVER_ADDR, self.domain_name)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_add_add_delete_delete(self):
|
def test_add_add_delete_delete(self):
|
||||||
self.add(self.ns1_name, self.ns1_records)
|
self.add(self.ns1_name, self.ns1_records)
|
||||||
self.add(self.ns2_name, self.ns2_records)
|
self.add(self.ns2_name, self.ns2_records)
|
||||||
|
@ -227,7 +222,6 @@ class DomainTestMixin(object):
|
||||||
self.validate_delete(self.MASTER_SERVER_ADDR, self.django_domain_name)
|
self.validate_delete(self.MASTER_SERVER_ADDR, self.django_domain_name)
|
||||||
self.validate_delete(self.SLAVE_SERVER_ADDR, self.django_domain_name)
|
self.validate_delete(self.SLAVE_SERVER_ADDR, self.django_domain_name)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_bad_creation(self):
|
def test_bad_creation(self):
|
||||||
self.assertRaises((self.rest.ResponseStatusError, AssertionError),
|
self.assertRaises((self.rest.ResponseStatusError, AssertionError),
|
||||||
self.add, self.domain_name, self.domain_records)
|
self.add, self.domain_name, self.domain_records)
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
|
||||||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.contrib.admin.utils import unquote
|
|
||||||
from django.contrib.admin.templatetags.admin_static import static
|
from django.contrib.admin.templatetags.admin_static import static
|
||||||
|
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||||
|
from django.contrib.admin.utils import unquote
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.utils import admin_link, admin_date
|
from orchestra.admin.utils import admin_date, admin_link
|
||||||
|
|
||||||
|
|
||||||
class LogEntryAdmin(admin.ModelAdmin):
|
class LogEntryAdmin(admin.ModelAdmin):
|
||||||
|
@ -34,11 +36,12 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
user_link = admin_link('user')
|
user_link = admin_link('user')
|
||||||
display_action_time = admin_date('action_time', short_description=_("Time"))
|
display_action_time = admin_date('action_time', short_description=_("Time"))
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_message(self, log):
|
def display_message(self, log):
|
||||||
edit = '<a href="%(url)s"><img src="%(img)s"></img></a>' % {
|
edit = format_html('<a href="{url}"><img src="{img}"></img></a>', **{
|
||||||
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
|
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
|
||||||
'img': static('admin/img/icon-changelink.svg'),
|
'img': static('admin/img/icon-changelink.svg'),
|
||||||
}
|
})
|
||||||
if log.is_addition():
|
if log.is_addition():
|
||||||
return _('Added "%(link)s". %(edit)s') % {
|
return _('Added "%(link)s". %(edit)s') % {
|
||||||
'link': self.content_object_link(log),
|
'link': self.content_object_link(log),
|
||||||
|
@ -57,7 +60,6 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
}
|
}
|
||||||
display_message.short_description = _("Message")
|
display_message.short_description = _("Message")
|
||||||
display_message.admin_order_field = 'action_flag'
|
display_message.admin_order_field = 'action_flag'
|
||||||
display_message.allow_tags = True
|
|
||||||
|
|
||||||
def display_action(self, log):
|
def display_action(self, log):
|
||||||
if log.is_addition():
|
if log.is_addition():
|
||||||
|
@ -75,10 +77,9 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
url = reverse(view, args=(log.object_id,))
|
url = reverse(view, args=(log.object_id,))
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return log.object_repr
|
return log.object_repr
|
||||||
return '<a href="%s">%s</a>' % (url, log.object_repr)
|
return format_html('<a href="{}">{}</a>', url, log.object_repr)
|
||||||
content_object_link.short_description = _("Content object")
|
content_object_link.short_description = _("Content object")
|
||||||
content_object_link.admin_order_field = 'object_repr'
|
content_object_link.admin_order_field = 'object_repr'
|
||||||
content_object_link.allow_tags = True
|
|
||||||
|
|
||||||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
""" Add rel_opts and object to context """
|
""" Add rel_opts and object to context """
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import format_html, strip_tags
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ class MessageReadOnlyInline(admin.TabularInline):
|
||||||
'all': ('orchestra/css/hide-inline-id.css',)
|
'all': ('orchestra/css/hide-inline-id.css',)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def content_html(self, msg):
|
def content_html(self, msg):
|
||||||
context = {
|
context = {
|
||||||
'number': msg.number,
|
'number': msg.number,
|
||||||
|
@ -58,12 +60,13 @@ class MessageReadOnlyInline(admin.TabularInline):
|
||||||
}
|
}
|
||||||
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
||||||
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
header = '<strong style="color:#666;">%s</strong><hr />' % summary
|
||||||
|
|
||||||
content = markdown(msg.content)
|
content = markdown(msg.content)
|
||||||
content = content.replace('>\n', '>')
|
content = content.replace('>\n', '>')
|
||||||
content = '<div style="padding-left:20px;">%s</div>' % content
|
content = '<div style="padding-left:20px;">%s</div>' % content
|
||||||
|
|
||||||
return header + content
|
return header + content
|
||||||
content_html.short_description = _("Content")
|
content_html.short_description = _("Content")
|
||||||
content_html.allow_tags = True
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
@ -111,10 +114,10 @@ class TicketInline(admin.TabularInline):
|
||||||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def ticket_id(self, instance):
|
def ticket_id(self, instance):
|
||||||
return '<b>%s</b>' % admin_link()(instance)
|
return '<b>%s</b>' % admin_link()(instance)
|
||||||
ticket_id.short_description = '#'
|
ticket_id.short_description = '#'
|
||||||
ticket_id.allow_tags = True
|
|
||||||
|
|
||||||
|
|
||||||
class TicketAdmin(ExtendedModelAdmin):
|
class TicketAdmin(ExtendedModelAdmin):
|
||||||
|
@ -135,7 +138,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
'owner__username'
|
'owner__username'
|
||||||
)
|
)
|
||||||
actions = (
|
actions = (
|
||||||
mark_as_unread, mark_as_read, 'delete_selected', reject_tickets,
|
mark_as_unread, mark_as_read, reject_tickets,
|
||||||
resolve_tickets, close_tickets, take_tickets
|
resolve_tickets, close_tickets, take_tickets
|
||||||
)
|
)
|
||||||
sudo_actions = ('delete_selected',)
|
sudo_actions = ('delete_selected',)
|
||||||
|
@ -192,6 +195,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_summary(self, ticket):
|
def display_summary(self, ticket):
|
||||||
context = {
|
context = {
|
||||||
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
||||||
|
@ -207,14 +211,12 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
||||||
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
|
||||||
display_summary.short_description = 'Summary'
|
display_summary.short_description = 'Summary'
|
||||||
display_summary.allow_tags = True
|
|
||||||
|
|
||||||
def unbold_id(self, ticket):
|
def unbold_id(self, ticket):
|
||||||
""" Unbold id if ticket is read """
|
""" Unbold id if ticket is read """
|
||||||
if ticket.is_read_by(self.user):
|
if ticket.is_read_by(self.user):
|
||||||
return '<span style="font-weight:normal;font-size:11px;">%s</span>' % ticket.pk
|
return format_html('<span style="font-weight:normal;font-size:11px;">{}</span>', ticket.pk)
|
||||||
return ticket.pk
|
return ticket.pk
|
||||||
unbold_id.allow_tags = True
|
|
||||||
unbold_id.short_description = "#"
|
unbold_id.short_description = "#"
|
||||||
unbold_id.admin_order_field = 'id'
|
unbold_id.admin_order_field = 'id'
|
||||||
|
|
||||||
|
@ -222,8 +224,7 @@ class TicketAdmin(ExtendedModelAdmin):
|
||||||
""" Bold subject when tickets are unread for request.user """
|
""" Bold subject when tickets are unread for request.user """
|
||||||
if ticket.is_read_by(self.user):
|
if ticket.is_read_by(self.user):
|
||||||
return ticket.subject
|
return ticket.subject
|
||||||
return "<strong class='unread'>%s</strong>" % ticket.subject
|
return format_html("<strong class='unread'>{}</strong>", ticket.subject)
|
||||||
bold_subject.allow_tags = True
|
|
||||||
bold_subject.short_description = _("Subject")
|
bold_subject.short_description = _("Subject")
|
||||||
bold_subject.admin_order_field = 'subject'
|
bold_subject.admin_order_field = 'subject'
|
||||||
|
|
||||||
|
@ -297,10 +298,9 @@ class QueueAdmin(admin.ModelAdmin):
|
||||||
num = queue.tickets__count
|
num = queue.tickets__count
|
||||||
url = reverse('admin:issues_ticket_changelist')
|
url = reverse('admin:issues_ticket_changelist')
|
||||||
url += '?queue=%i' % queue.pk
|
url += '?queue=%i' % queue.pk
|
||||||
return '<a href="%s">%d</a>' % (url, num)
|
return format_html('<a href="{}">{}</a>', url, num)
|
||||||
num_tickets.short_description = _("Tickets")
|
num_tickets.short_description = _("Tickets")
|
||||||
num_tickets.admin_order_field = 'tickets__count'
|
num_tickets.admin_order_field = 'tickets__count'
|
||||||
num_tickets.allow_tags = True
|
|
||||||
|
|
||||||
def get_list_display(self, request):
|
def get_list_display(self, request):
|
||||||
""" show notifications """
|
""" show notifications """
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import viewsets, mixins
|
from rest_framework import viewsets, mixins
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from orchestra.api import router, LogApiMixin
|
from orchestra.api import router, LogApiMixin
|
||||||
|
@ -13,13 +13,13 @@ class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
|
||||||
queryset = Ticket.objects.all()
|
queryset = Ticket.objects.all()
|
||||||
serializer_class = TicketSerializer
|
serializer_class = TicketSerializer
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def mark_as_read(self, request, pk=None):
|
def mark_as_read(self, request, pk=None):
|
||||||
ticket = self.get_object()
|
ticket = self.get_object()
|
||||||
ticket.mark_as_read_by(request.user)
|
ticket.mark_as_read_by(request.user)
|
||||||
return Response({'status': 'Ticket marked as read'})
|
return Response({'status': 'Ticket marked as read'})
|
||||||
|
|
||||||
@detail_route()
|
@action(detail=True)
|
||||||
def mark_as_unread(self, request, pk=None):
|
def mark_as_unread(self, request, pk=None):
|
||||||
ticket = self.get_object()
|
ticket = self.get_object()
|
||||||
ticket.mark_as_unread_by(request.user)
|
ticket.mark_as_unread_by(request.user)
|
||||||
|
|
|
@ -22,7 +22,7 @@ class MarkDownWidget(forms.Textarea):
|
||||||
)
|
)
|
||||||
markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text
|
markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text
|
||||||
|
|
||||||
def render(self, name, value, attrs):
|
def render(self, name, value, attrs, renderer=None):
|
||||||
widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name
|
widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name
|
||||||
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
textarea = super(MarkDownWidget, self).render(name, value, attrs)
|
||||||
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import orchestra.models.fields
|
import orchestra.models.fields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
||||||
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
||||||
('content', models.TextField(verbose_name='content')),
|
('content', models.TextField(verbose_name='content')),
|
||||||
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
||||||
('author', models.ForeignKey(related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'get_latest_by': 'id',
|
'get_latest_by': 'id',
|
||||||
|
@ -48,9 +49,9 @@ class Migration(migrations.Migration):
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
||||||
('creator', models.ForeignKey(related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||||
('owner', models.ForeignKey(blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
||||||
('queue', models.ForeignKey(blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
('queue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets', null=True, to='issues.Queue')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ['-updated_at'],
|
'ordering': ['-updated_at'],
|
||||||
|
@ -60,14 +61,14 @@ class Migration(migrations.Migration):
|
||||||
name='TicketTracker',
|
name='TicketTracker',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
|
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
|
||||||
('ticket', models.ForeignKey(related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
||||||
('user', models.ForeignKey(related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='message',
|
model_name='message',
|
||||||
name='ticket',
|
name='ticket',
|
||||||
field=models.ForeignKey(related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='tickettracker',
|
name='tickettracker',
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.utils.timezone import utc
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('issues', '0001_initial'), ('issues', '0002_auto_20150709_1018'), ('issues', '0003_auto_20160320_1127'), ('issues', '0004_auto_20170528_2011')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Message',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
|
||||||
|
('content', models.TextField(verbose_name='content')),
|
||||||
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Queue',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128, unique=True, verbose_name='name')),
|
||||||
|
('verbose_name', models.CharField(blank=True, max_length=128, verbose_name='verbose_name')),
|
||||||
|
('default', models.BooleanField(default=False, verbose_name='default')),
|
||||||
|
('notify', orchestra.models.fields.MultiSelectField(blank=True, choices=[('SUPPORT', 'Support tickets'), ('ADMIN', 'Administrative'), ('BILLING', 'Billing'), ('TECH', 'Technical'), ('ADDS', 'Announcements'), ('EMERGENCY', 'Emergency contact')], default=('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY'), help_text='Contacts to notify by email', max_length=256, verbose_name='notify')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Ticket',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('creator_name', models.CharField(blank=True, max_length=256, verbose_name='creator name')),
|
||||||
|
('subject', models.CharField(max_length=256, verbose_name='subject')),
|
||||||
|
('description', models.TextField(verbose_name='description')),
|
||||||
|
('priority', models.CharField(choices=[('HIGH', 'High'), ('MEDIUM', 'Medium'), ('LOW', 'Low')], default='MEDIUM', max_length=32, verbose_name='priority')),
|
||||||
|
('state', models.CharField(choices=[('NEW', 'New'), ('IN_PROGRESS', 'In Progress'), ('RESOLVED', 'Resolved'), ('FEEDBACK', 'Feedback'), ('REJECTED', 'Rejected'), ('CLOSED', 'Closed')], default='NEW', max_length=32, verbose_name='state')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
|
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
|
||||||
|
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
|
||||||
|
('queue', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='issues.Queue')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TicketTracker',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='ticket',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='tickettracker',
|
||||||
|
unique_together=set([('ticket', 'user')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='message',
|
||||||
|
name='created_on',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 20, 10, 27, 45, 766388, tzinfo=utc), verbose_name='created at'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='creator',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='queue',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='issues.Queue'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -161,10 +161,10 @@ class Ticket(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
|
ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
|
||||||
related_name='messages')
|
verbose_name=_("ticket"), related_name='messages')
|
||||||
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"),
|
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||||
related_name='ticket_messages')
|
verbose_name=_("author"), related_name='ticket_messages')
|
||||||
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
||||||
content = models.TextField(_("content"))
|
content = models.TextField(_("content"))
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||||
|
@ -191,9 +191,10 @@ class Message(models.Model):
|
||||||
|
|
||||||
class TicketTracker(models.Model):
|
class TicketTracker(models.Model):
|
||||||
""" Keeps track of user read tickets """
|
""" Keeps track of user read tickets """
|
||||||
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers')
|
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
|
||||||
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"),
|
verbose_name=_("ticket"), related_name='trackers')
|
||||||
related_name='ticket_trackers')
|
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("user"), related_name='ticket_trackers')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (
|
unique_together = (
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,8 +23,8 @@ class Migration(migrations.Migration):
|
||||||
('address_name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name', blank=True)),
|
('address_name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name', blank=True)),
|
||||||
('admin_email', models.EmailField(max_length=254, verbose_name='admin email', help_text='Administration email address')),
|
('admin_email', models.EmailField(max_length=254, verbose_name='admin email', help_text='Administration email address')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.')),
|
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.')),
|
||||||
('account', models.ForeignKey(related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
('address_domain', models.ForeignKey(null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
('address_domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('lists', '0001_initial'), ('lists', '0002_auto_20160912_1221'), ('lists', '0003_auto_20160912_1241'), ('lists', '0004_auto_20210330_1049')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='List',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Default list address <name>@lists.orchestra.lan', max_length=128, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
|
||||||
|
('address_name', models.CharField(blank=True, max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name')),
|
||||||
|
('admin_email', models.EmailField(help_text='Administration email address', max_length=254, verbose_name='admin email')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('address_domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='domains.Domain', verbose_name='address domain')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='list',
|
||||||
|
unique_together=set([('address_name', 'address_domain')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='address_domain',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='domains.Domain', verbose_name='address domain'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='address_name',
|
||||||
|
field=models.CharField(blank=True, max_length=52, validators=[orchestra.core.validators.validate_name], verbose_name='address name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@grups.pangea.org', max_length=52, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='address_name',
|
||||||
|
field=models.CharField(blank=True, max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='address name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@grups.pangea.org', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@lists.orchestra.lan', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-03-30 10:49
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('lists', '0003_auto_20160912_1241'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='list',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Default list address <name>@lists.orchestra.lan', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -30,7 +30,7 @@ class List(models.Model):
|
||||||
admin_email = models.EmailField(_("admin email"),
|
admin_email = models.EmailField(_("admin email"),
|
||||||
help_text=_("Administration email address"))
|
help_text=_("Administration email address"))
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='lists')
|
related_name='lists', on_delete=models.CASCADE)
|
||||||
# TODO also admin
|
# TODO also admin
|
||||||
is_active = models.BooleanField(_("active"), default=True,
|
is_active = models.BooleanField(_("active"), default=True,
|
||||||
help_text=_("Designates whether this account should be treated as active. "
|
help_text=_("Designates whether this account should be treated as active. "
|
||||||
|
|
|
@ -12,7 +12,7 @@ from .models import List
|
||||||
|
|
||||||
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = List.address_domain.field.rel.to
|
model = List.address_domain.field.related_model
|
||||||
fields = ('url', 'id', 'name')
|
fields = ('url', 'id', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import os
|
import os
|
||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
import requests
|
import unittest
|
||||||
from unittest import skip
|
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
import requests
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
|
||||||
|
|
||||||
from orchestra.admin.utils import change_url
|
from orchestra.admin.utils import change_url
|
||||||
from orchestra.contrib.domains.models import Domain
|
from orchestra.contrib.domains.models import Domain
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Route, Server
|
||||||
from orchestra.utils.sys import sshrun
|
from orchestra.utils.sys import sshrun
|
||||||
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, snapshot_on_error,
|
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
|
||||||
save_response_on_error)
|
save_response_on_error, snapshot_on_error)
|
||||||
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from ... import backends, settings
|
from ... import backends, settings
|
||||||
from ...models import List
|
from ...models import List
|
||||||
|
|
||||||
|
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||||
|
|
||||||
|
|
||||||
class ListMixin(object):
|
class ListMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||||
|
@ -31,11 +32,8 @@ class ListMixin(object):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ListMixin, self).setUp()
|
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()
|
self.add_route()
|
||||||
|
djsettings.DEBUG = True
|
||||||
|
|
||||||
def validate_add(self, name, address=None):
|
def validate_add(self, name, address=None):
|
||||||
sshrun(self.MASTER_SERVER, 'list_members %s' % name, display=False)
|
sshrun(self.MASTER_SERVER, 'list_members %s' % name, display=False)
|
||||||
|
@ -86,7 +84,6 @@ class ListMixin(object):
|
||||||
backend = backends.MailmanController.get_name()
|
backend = backends.MailmanController.get_name()
|
||||||
Route.objects.create(backend=backend, match=True, host=server)
|
Route.objects.create(backend=backend, match=True, host=server)
|
||||||
|
|
||||||
# @skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -96,7 +93,6 @@ class ListMixin(object):
|
||||||
self.validate_login(name, password)
|
self.validate_login(name, password)
|
||||||
self.addCleanup(self.delete, name)
|
self.addCleanup(self.delete, name)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_add_with_address(self):
|
def test_add_with_address(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -109,7 +105,6 @@ class ListMixin(object):
|
||||||
# Mailman doesn't support changing the address, only the domain
|
# Mailman doesn't support changing the address, only the domain
|
||||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_change_password(self):
|
def test_change_password(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -121,7 +116,6 @@ class ListMixin(object):
|
||||||
self.change_password(name, new_password)
|
self.change_password(name, new_password)
|
||||||
self.validate_login(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):
|
def test_change_domain(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -137,7 +131,6 @@ class ListMixin(object):
|
||||||
self.update_domain(name, domain_name)
|
self.update_domain(name, domain_name)
|
||||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_change_address_name(self):
|
def test_change_address_name(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -152,7 +145,6 @@ class ListMixin(object):
|
||||||
self.update_address_name(name, address_name)
|
self.update_address_name(name, address_name)
|
||||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
name = '%s_list' % random_ascii(10)
|
name = '%s_list' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -160,8 +152,7 @@ class ListMixin(object):
|
||||||
address_name = '%s_name' % random_ascii(10)
|
address_name = '%s_name' % random_ascii(10)
|
||||||
domain_name = '%sdomain.lan' % random_ascii(10)
|
domain_name = '%sdomain.lan' % random_ascii(10)
|
||||||
address_domain = Domain.objects.create(name=domain_name, account=self.account)
|
address_domain = Domain.objects.create(name=domain_name, account=self.account)
|
||||||
self.add(name, password, admin_email, address_name=address_name,
|
self.add(name, password, admin_email, address_name=address_name, address_domain=address_domain)
|
||||||
address_domain=address_domain)
|
|
||||||
# Mailman doesn't support changing the address, only the domain
|
# Mailman doesn't support changing the address, only the domain
|
||||||
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
|
||||||
self.delete(name)
|
self.delete(name)
|
||||||
|
@ -169,6 +160,7 @@ class ListMixin(object):
|
||||||
self.validate_delete(name)
|
self.validate_delete(name)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||||
class RESTListMixin(ListMixin):
|
class RESTListMixin(ListMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTListMixin, self).setUp()
|
super(RESTListMixin, self).setUp()
|
||||||
|
@ -236,8 +228,6 @@ class AdminListMixin(ListMixin):
|
||||||
domain_select.select_by_value(str(domain.pk))
|
domain_select.select_by_value(str(domain.pk))
|
||||||
|
|
||||||
name_field.submit()
|
name_field.submit()
|
||||||
# import pdb; pdb.set_trace()
|
|
||||||
# oop = Server.objects.all()
|
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
@snapshot_on_error
|
@snapshot_on_error
|
||||||
|
|
|
@ -3,9 +3,10 @@ from urllib.parse import parse_qs
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import F, Count, Value as V
|
from django.db.models import F, Count, Value as V
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
|
from django.utils.html import format_html, format_html_join
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -82,6 +83,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
if settings.MAILBOXES_LOCAL_DOMAIN:
|
if settings.MAILBOXES_LOCAL_DOMAIN:
|
||||||
type(self).actions = self.actions + (SendMailboxEmail(),)
|
type(self).actions = self.actions + (SendMailboxEmail(),)
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_addresses(self, mailbox):
|
def display_addresses(self, mailbox):
|
||||||
# Get from forwards
|
# Get from forwards
|
||||||
cache = caches.get_request_cache()
|
cache = caches.get_request_cache()
|
||||||
|
@ -93,7 +95,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
qs = qs.values_list('id', 'email', 'forward')
|
qs = qs.values_list('id', 'email', 'forward')
|
||||||
for addr_id, email, mbox in qs:
|
for addr_id, email, mbox in qs:
|
||||||
url = reverse('admin:mailboxes_address_change', args=(addr_id,))
|
url = reverse('admin:mailboxes_address_change', args=(addr_id,))
|
||||||
link = '<a href="%s">%s</a>' % (url, email)
|
link = format_html('<a href="{}">{}</a>', url, email)
|
||||||
try:
|
try:
|
||||||
cached_forwards[mbox].append(link)
|
cached_forwards[mbox].append(link)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -107,26 +109,23 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
addresses = []
|
addresses = []
|
||||||
for addr in mailbox.addresses.all():
|
for addr in mailbox.addresses.all():
|
||||||
url = change_url(addr)
|
url = change_url(addr)
|
||||||
addresses.append('<a href="%s">%s</a>' % (url, addr.email))
|
addresses.append(format_html('<a href="{}">{}</a>', url, addr.email))
|
||||||
return '<br>'.join(addresses+forwards)
|
return '<br>'.join(addresses+forwards)
|
||||||
display_addresses.short_description = _("Addresses")
|
display_addresses.short_description = _("Addresses")
|
||||||
display_addresses.allow_tags = True
|
|
||||||
|
|
||||||
def display_forwards(self, mailbox):
|
def display_forwards(self, mailbox):
|
||||||
forwards = []
|
forwards = mailbox.get_forwards()
|
||||||
for addr in mailbox.get_forwards():
|
return format_html_join(
|
||||||
url = change_url(addr)
|
'<br>', '<a href="{}">{}</a>',
|
||||||
forwards.append('<a href="%s">%s</a>' % (url, addr.email))
|
[(change_url(addr), addr.email) for addr in forwards]
|
||||||
return '<br>'.join(forwards)
|
)
|
||||||
display_forwards.short_description = _("Forward from")
|
display_forwards.short_description = _("Forward from")
|
||||||
display_forwards.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_filtering(self, mailbox):
|
def display_filtering(self, mailbox):
|
||||||
""" becacuse of allow_tags = True """
|
|
||||||
return mailbox.get_filtering_display()
|
return mailbox.get_filtering_display()
|
||||||
display_filtering.short_description = _("Filtering")
|
display_filtering.short_description = _("Filtering")
|
||||||
display_filtering.admin_order_field = 'filtering'
|
display_filtering.admin_order_field = 'filtering'
|
||||||
display_filtering.allow_tags = True
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
if db_field.name == 'filtering':
|
if db_field.name == 'filtering':
|
||||||
|
@ -217,7 +216,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
elif obj.custom_filtering:
|
elif obj.custom_filtering:
|
||||||
messages.warning(request, msg)
|
messages.warning(request, msg)
|
||||||
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.addresses = form.cleaned_data['addresses']
|
obj.addresses.set(form.cleaned_data['addresses'])
|
||||||
|
|
||||||
|
|
||||||
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
@ -247,29 +246,27 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
|
||||||
def email_link(self, address):
|
def email_link(self, address):
|
||||||
link = self.domain_link(address)
|
link = self.domain_link(address)
|
||||||
return "%s@%s" % (address.name, link)
|
return format_html("{}@{}", address.name, link)
|
||||||
email_link.short_description = _("Email")
|
email_link.short_description = _("Email")
|
||||||
email_link.allow_tags = True
|
|
||||||
|
|
||||||
def display_mailboxes(self, address):
|
def display_mailboxes(self, address):
|
||||||
boxes = []
|
boxes = address.mailboxes.all()
|
||||||
for mailbox in address.mailboxes.all():
|
return format_html_join(
|
||||||
url = change_url(mailbox)
|
mark_safe('<br>'), '<a href="{}">{}</a>',
|
||||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
|
||||||
return '<br>'.join(boxes)
|
)
|
||||||
display_mailboxes.short_description = _("Mailboxes")
|
display_mailboxes.short_description = _("Mailboxes")
|
||||||
display_mailboxes.allow_tags = True
|
|
||||||
display_mailboxes.admin_order_field = 'mailboxes__count'
|
display_mailboxes.admin_order_field = 'mailboxes__count'
|
||||||
|
|
||||||
def display_all_mailboxes(self, address):
|
def display_all_mailboxes(self, address):
|
||||||
boxes = []
|
boxes = address.get_mailboxes()
|
||||||
for mailbox in address.get_mailboxes():
|
return format_html_join(
|
||||||
url = change_url(mailbox)
|
mark_safe('<br>'), '<a href="{}">{}</a>',
|
||||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
|
||||||
return '<br>'.join(boxes)
|
)
|
||||||
display_all_mailboxes.short_description = _("Mailboxes links")
|
display_all_mailboxes.short_description = _("Mailboxes links")
|
||||||
display_all_mailboxes.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_forward(self, address):
|
def display_forward(self, address):
|
||||||
forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()}
|
forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()}
|
||||||
values = []
|
values = []
|
||||||
|
@ -281,7 +278,6 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
values.append(forward)
|
values.append(forward)
|
||||||
return '<br>'.join(values)
|
return '<br>'.join(values)
|
||||||
display_forward.short_description = _("Forward")
|
display_forward.short_description = _("Forward")
|
||||||
display_forward.allow_tags = True
|
|
||||||
display_forward.admin_order_field = 'forward'
|
display_forward.admin_order_field = 'forward'
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from orchestra.api import router, SetPasswordApiMixin, LogApiMixin
|
||||||
from orchestra.contrib.accounts.api import AccountApiMixin
|
from orchestra.contrib.accounts.api import AccountApiMixin
|
||||||
|
|
||||||
from .models import Address, Mailbox
|
from .models import Address, Mailbox
|
||||||
from .serializers import AddressSerializer, MailboxSerializer
|
from .serializers import AddressSerializer, MailboxSerializer, MailboxWritableSerializer
|
||||||
|
|
||||||
|
|
||||||
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
|
||||||
|
@ -17,6 +17,12 @@ class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets
|
||||||
queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
|
queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
|
||||||
serializer_class = MailboxSerializer
|
serializer_class = MailboxSerializer
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
return self.serializer_class
|
||||||
|
|
||||||
|
return MailboxWritableSerializer
|
||||||
|
|
||||||
|
|
||||||
router.register(r'mailboxes', MailboxViewSet)
|
router.register(r'mailboxes', MailboxViewSet)
|
||||||
router.register(r'addresses', AddressViewSet)
|
router.register(r'addresses', AddressViewSet)
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.contrib.mailboxes.validators
|
import orchestra.contrib.mailboxes.validators
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
|
||||||
|
@ -21,8 +22,8 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(verbose_name='name', validators=[orchestra.contrib.mailboxes.validators.validate_emailname], blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64)),
|
('name', models.CharField(verbose_name='name', validators=[orchestra.contrib.mailboxes.validators.validate_emailname], blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64)),
|
||||||
('forward', models.CharField(verbose_name='forward', validators=[orchestra.contrib.mailboxes.validators.validate_forward], blank=True, help_text='Space separated email addresses or mailboxes', max_length=256)),
|
('forward', models.CharField(verbose_name='forward', validators=[orchestra.contrib.mailboxes.validators.validate_forward], blank=True, help_text='Space separated email addresses or mailboxes', max_length=256)),
|
||||||
('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
|
||||||
('domain', models.ForeignKey(to='domains.Domain', related_name='addresses', verbose_name='domain')),
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Domain', related_name='addresses', verbose_name='domain')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'addresses',
|
'verbose_name_plural': 'addresses',
|
||||||
|
@ -35,7 +36,7 @@ class Migration(migrations.Migration):
|
||||||
('subject', models.CharField(verbose_name='subject', max_length=256)),
|
('subject', models.CharField(verbose_name='subject', max_length=256)),
|
||||||
('message', models.TextField(verbose_name='message')),
|
('message', models.TextField(verbose_name='message')),
|
||||||
('enabled', models.BooleanField(verbose_name='enabled', default=False)),
|
('enabled', models.BooleanField(verbose_name='enabled', default=False)),
|
||||||
('address', models.OneToOneField(to='mailboxes.Address', related_name='autoresponse', verbose_name='address')),
|
('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='mailboxes.Address', related_name='autoresponse', verbose_name='address')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -47,7 +48,7 @@ class Migration(migrations.Migration):
|
||||||
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('REDIRECT', 'Archive spam (X-Spam-Score≥9)'), ('DISABLE', 'Disable'), ('REJECT', 'Reject spam (X-Spam-Score≥9)')], max_length=16, default='REDIRECT')),
|
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('REDIRECT', 'Archive spam (X-Spam-Score≥9)'), ('DISABLE', 'Disable'), ('REJECT', 'Reject spam (X-Spam-Score≥9)')], max_length=16, default='REDIRECT')),
|
||||||
('custom_filtering', models.TextField(verbose_name='filtering', validators=[orchestra.contrib.mailboxes.validators.validate_sieve], blank=True, help_text='Arbitrary email filtering in sieve language. This overrides any automatic junk email filtering')),
|
('custom_filtering', models.TextField(verbose_name='filtering', validators=[orchestra.contrib.mailboxes.validators.validate_sieve], blank=True, help_text='Arbitrary email filtering in sieve language. This overrides any automatic junk email filtering')),
|
||||||
('is_active', models.BooleanField(verbose_name='active', default=True)),
|
('is_active', models.BooleanField(verbose_name='active', default=True)),
|
||||||
('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='mailboxes', verbose_name='account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, related_name='mailboxes', verbose_name='account')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'mailboxes',
|
'verbose_name_plural': 'mailboxes',
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:27
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.contrib.mailboxes.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('mailboxes', '0001_initial'), ('mailboxes', '0002_auto_20160219_1032'), ('mailboxes', '0003_auto_20170528_2011')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('domains', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Address',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64, validators=[orchestra.contrib.mailboxes.validators.validate_emailname], verbose_name='name')),
|
||||||
|
('forward', models.CharField(blank=True, help_text='Space separated email addresses or mailboxes', max_length=256, validators=[orchestra.contrib.mailboxes.validators.validate_forward], verbose_name='forward')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='domains.Domain', verbose_name='domain')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'addresses',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Autoresponse',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('subject', models.CharField(max_length=256, verbose_name='subject')),
|
||||||
|
('message', models.TextField(verbose_name='message')),
|
||||||
|
('enabled', models.BooleanField(default=False, verbose_name='enabled')),
|
||||||
|
('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='autoresponse', to='mailboxes.Address', verbose_name='address')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Mailbox',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(db_index=True, help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid mailbox name.')], verbose_name='name')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('DISABLE', 'Disable'), ('REDIRECT', 'Archive spam (Score≥8)'), ('REDIRECT5', 'Archive spam (Score≥5)'), ('REJECT', 'Reject spam (Score≥8)'), ('REJECT5', 'Reject spam (Score≥5)')], default='REDIRECT', max_length=16)),
|
||||||
|
('custom_filtering', models.TextField(blank=True, help_text="Arbitrary email filtering in <a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. This overrides any automatic junk email filtering", validators=[orchestra.contrib.mailboxes.validators.validate_sieve], verbose_name='filtering')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='active')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mailboxes', to=settings.AUTH_USER_MODEL, verbose_name='account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'mailboxes',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='address',
|
||||||
|
name='mailboxes',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='addresses', to='mailboxes.Mailbox', verbose_name='mailboxes'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='address',
|
||||||
|
unique_together=set([('name', 'domain')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -23,7 +23,7 @@ class Mailbox(models.Model):
|
||||||
])
|
])
|
||||||
password = models.CharField(_("password"), max_length=128)
|
password = models.CharField(_("password"), max_length=128)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='mailboxes')
|
related_name='mailboxes', on_delete=models.CASCADE)
|
||||||
filtering = models.CharField(max_length=16,
|
filtering = models.CharField(max_length=16,
|
||||||
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
|
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
|
||||||
choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())])
|
choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())])
|
||||||
|
@ -44,7 +44,7 @@ class Mailbox(models.Model):
|
||||||
def active(self):
|
def active(self):
|
||||||
try:
|
try:
|
||||||
return self.is_active and self.account.is_active
|
return self.is_active and self.account.is_active
|
||||||
except type(self).account.field.rel.to.DoesNotExist:
|
except type(self).account.field.related_model.DoesNotExist:
|
||||||
return self.is_active
|
return self.is_active
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
|
@ -97,14 +97,14 @@ class Address(models.Model):
|
||||||
validators=[validators.validate_emailname],
|
validators=[validators.validate_emailname],
|
||||||
help_text=_("Address name, left blank for a <i>catch-all</i> address"))
|
help_text=_("Address name, left blank for a <i>catch-all</i> address"))
|
||||||
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
|
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
|
||||||
verbose_name=_("domain"), related_name='addresses')
|
verbose_name=_("domain"), related_name='addresses', on_delete=models.CASCADE)
|
||||||
mailboxes = models.ManyToManyField(Mailbox, verbose_name=_("mailboxes"),
|
mailboxes = models.ManyToManyField(Mailbox, verbose_name=_("mailboxes"),
|
||||||
related_name='addresses', blank=True)
|
related_name='addresses', blank=True)
|
||||||
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
||||||
validators=[validators.validate_forward],
|
validators=[validators.validate_forward],
|
||||||
help_text=_("Space separated email addresses or mailboxes"))
|
help_text=_("Space separated email addresses or mailboxes"))
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='addresses')
|
related_name='addresses', on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("addresses")
|
verbose_name_plural = _("addresses")
|
||||||
|
@ -168,7 +168,7 @@ class Address(models.Model):
|
||||||
|
|
||||||
class Autoresponse(models.Model):
|
class Autoresponse(models.Model):
|
||||||
address = models.OneToOneField(Address, verbose_name=_("address"),
|
address = models.OneToOneField(Address, verbose_name=_("address"),
|
||||||
related_name='autoresponse')
|
related_name='autoresponse', on_delete=models.CASCADE)
|
||||||
# TODO initial_date
|
# TODO initial_date
|
||||||
subject = models.CharField(_("subject"), max_length=256)
|
subject = models.CharField(_("subject"), max_length=256)
|
||||||
message = models.TextField(_("message"))
|
message = models.TextField(_("message"))
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.db import transaction
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer
|
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer
|
||||||
|
@ -8,7 +9,7 @@ from .models import Mailbox, Address
|
||||||
|
|
||||||
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Address.domain.field.rel.to
|
model = Address.domain.field.related_model
|
||||||
fields = ('url', 'id', 'name')
|
fields = ('url', 'id', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +36,41 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer
|
||||||
postonly_fields = ('name', 'password')
|
postonly_fields = ('name', 'password')
|
||||||
|
|
||||||
|
|
||||||
|
class AddressRelatedField(serializers.HyperlinkedRelatedField):
|
||||||
|
# Filter addresses by account (user)
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = super().get_queryset()
|
||||||
|
return qs.filter(account=self.context['account'])
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxWritableSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
|
||||||
|
addresses = AddressRelatedField(many=True, view_name='address-detail', queryset=Address.objects.all())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Mailbox
|
||||||
|
fields = (
|
||||||
|
'url', 'id', 'name', 'password', 'filtering', 'custom_filtering', 'addresses', 'is_active'
|
||||||
|
)
|
||||||
|
postonly_fields = ('name', 'password')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['addresses'].context['account'] = self.account
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
addresses = validated_data.pop('addresses', [])
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
instance.addresses.set(addresses)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
addresses = validated_data.pop('addresses', [])
|
||||||
|
instance.addresses.set(addresses)
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mailbox
|
model = Mailbox
|
||||||
|
@ -43,7 +79,7 @@ class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSe
|
||||||
|
|
||||||
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
domain = RelatedDomainSerializer()
|
domain = RelatedDomainSerializer()
|
||||||
mailboxes = RelatedMailboxSerializer(many=True, required=False) #allow_add_remove=True
|
mailboxes = RelatedMailboxSerializer(many=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Address
|
model = Address
|
||||||
|
@ -51,6 +87,21 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
attrs = super(AddressSerializer, self).validate(attrs)
|
attrs = super(AddressSerializer, self).validate(attrs)
|
||||||
if not attrs['mailboxes'] and not attrs['forward']:
|
mailboxes = attrs.get('mailboxes', [])
|
||||||
|
forward = attrs.get('forward', '')
|
||||||
|
if not mailboxes and not forward:
|
||||||
raise serializers.ValidationError("A mailbox or forward address should be provided.")
|
raise serializers.ValidationError("A mailbox or forward address should be provided.")
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
mailboxes = validated_data.pop('mailboxes', [])
|
||||||
|
obj = super().create(validated_data)
|
||||||
|
obj.mailboxes.set(mailboxes)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
mailboxes = validated_data.pop('mailboxes', [])
|
||||||
|
instance.mailboxes.set(mailboxes)
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
|
@ -27,7 +27,7 @@ def create_local_address(sender, *args, **kwargs):
|
||||||
mbox = kwargs['instance']
|
mbox = kwargs['instance']
|
||||||
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
if not mbox.pk and local_domain:
|
if not mbox.pk and local_domain:
|
||||||
Domain = Address._meta.get_field('domain').rel.to
|
Domain = Address._meta.get_field('domain').remote_field.model
|
||||||
try:
|
try:
|
||||||
domain = Domain.objects.get(name=local_domain)
|
domain = Domain.objects.get(name=local_domain)
|
||||||
except Domain.DoesNotExist:
|
except Domain.DoesNotExist:
|
||||||
|
|
|
@ -4,14 +4,14 @@ import poplib
|
||||||
import smtplib
|
import smtplib
|
||||||
import time
|
import time
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import unittest
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from unittest import skip
|
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings as djsettings
|
from django.conf import settings as djsettings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
from orchestra.contrib.orchestration.models import Server, Route
|
from orchestra.contrib.orchestration.models import Server, Route
|
||||||
|
@ -22,6 +22,8 @@ from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot
|
||||||
from ... import backends, settings
|
from ... import backends, settings
|
||||||
from ...models import Mailbox
|
from ...models import Mailbox
|
||||||
|
|
||||||
|
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
|
||||||
|
|
||||||
|
|
||||||
class MailboxMixin(object):
|
class MailboxMixin(object):
|
||||||
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
|
||||||
|
@ -49,6 +51,7 @@ class MailboxMixin(object):
|
||||||
Resource.objects.create(
|
Resource.objects.create(
|
||||||
name='disk',
|
name='disk',
|
||||||
content_type=ContentType.objects.get_for_model(Mailbox),
|
content_type=ContentType.objects.get_for_model(Mailbox),
|
||||||
|
period=Resource.LAST,
|
||||||
verbose_name='Mail quota',
|
verbose_name='Mail quota',
|
||||||
unit='MB',
|
unit='MB',
|
||||||
scale=10**6,
|
scale=10**6,
|
||||||
|
@ -108,7 +111,6 @@ class MailboxMixin(object):
|
||||||
home = Mailbox.objects.get(name=username).get_home()
|
home = Mailbox.objects.get(name=username).get_home()
|
||||||
sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False)
|
sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -117,7 +119,6 @@ class MailboxMixin(object):
|
||||||
imap = self.login_imap(username, password)
|
imap = self.login_imap(username, password)
|
||||||
self.validate_mailbox(username)
|
self.validate_mailbox(username)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_change_password(self):
|
def test_change_password(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -128,7 +129,6 @@ class MailboxMixin(object):
|
||||||
self.change_password(username, new_password)
|
self.change_password(username, new_password)
|
||||||
imap = self.login_imap(username, new_password)
|
imap = self.login_imap(username, new_password)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_quota(self):
|
def test_quota(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -143,7 +143,6 @@ class MailboxMixin(object):
|
||||||
imap_quota = int(imap.getquotaroot("INBOX")[1][1][0].split(' ')[-1].split(')')[0])
|
imap_quota = int(imap.getquotaroot("INBOX")[1][1][0].split(' ')[-1].split(')')[0])
|
||||||
self.assertEqual(quota*1024, imap_quota)
|
self.assertEqual(quota*1024, imap_quota)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_send_email(self):
|
def test_send_email(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -160,7 +159,6 @@ class MailboxMixin(object):
|
||||||
finally:
|
finally:
|
||||||
server.quit()
|
server.quit()
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_address(self):
|
def test_address(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -174,7 +172,6 @@ class MailboxMixin(object):
|
||||||
self.send_email("%s@%s" % (name, domain), token)
|
self.send_email("%s@%s" % (name, domain), token)
|
||||||
self.validate_email(username, token)
|
self.validate_email(username, token)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_disable(self):
|
def test_disable(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -185,7 +182,6 @@ class MailboxMixin(object):
|
||||||
self.disable(username)
|
self.disable(username)
|
||||||
self.assertRaises(imap.error, self.login_imap, username, password)
|
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):
|
def test_delete(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%sppppP001' % random_ascii(5)
|
password = '@!?%sppppP001' % random_ascii(5)
|
||||||
|
@ -201,7 +197,6 @@ class MailboxMixin(object):
|
||||||
self.assertRaises(CommandError,
|
self.assertRaises(CommandError,
|
||||||
sshrun, self.MASTER_SERVER, 'ls %s' % home, display=False)
|
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):
|
def test_delete_address(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -218,7 +213,6 @@ class MailboxMixin(object):
|
||||||
self.send_email("%s@%s" % (name, domain), token)
|
self.send_email("%s@%s" % (name, domain), token)
|
||||||
self.validate_email(username, token)
|
self.validate_email(username, token)
|
||||||
|
|
||||||
@skip("Skip because not exists get_auth_token in orm.api.Api")
|
|
||||||
def test_custom_filtering(self):
|
def test_custom_filtering(self):
|
||||||
username = '%s_mailbox' % random_ascii(10)
|
username = '%s_mailbox' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
@ -244,6 +238,7 @@ class MailboxMixin(object):
|
||||||
# TODO test autoreply
|
# TODO test autoreply
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_REST_API, "REST API tests")
|
||||||
class RESTMailboxMixin(MailboxMixin):
|
class RESTMailboxMixin(MailboxMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTMailboxMixin, self).setUp()
|
super(RESTMailboxMixin, self).setUp()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,11 @@ import email
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
|
@ -60,11 +62,10 @@ class MessageAdmin(ExtendedModelAdmin):
|
||||||
def display_subject(self, instance):
|
def display_subject(self, instance):
|
||||||
subject = instance.subject
|
subject = instance.subject
|
||||||
if len(subject) > 64:
|
if len(subject) > 64:
|
||||||
return subject[:64] + '…'
|
return mark_safe(subject[:64] + '…')
|
||||||
return subject
|
return subject
|
||||||
display_subject.short_description = _("Subject")
|
display_subject.short_description = _("Subject")
|
||||||
display_subject.admin_order_field = 'subject'
|
display_subject.admin_order_field = 'subject'
|
||||||
display_subject.allow_tags = True
|
|
||||||
|
|
||||||
def display_retries(self, instance):
|
def display_retries(self, instance):
|
||||||
num_logs = instance.logs__count
|
num_logs = instance.logs__count
|
||||||
|
@ -74,10 +75,9 @@ class MessageAdmin(ExtendedModelAdmin):
|
||||||
else:
|
else:
|
||||||
url = reverse('admin:mailer_smtplog_changelist')
|
url = reverse('admin:mailer_smtplog_changelist')
|
||||||
url += '?&message=%i' % instance.pk
|
url += '?&message=%i' % instance.pk
|
||||||
return '<a href="%s" onclick="return showAddAnotherPopup(this);">%d</a>' % (url, instance.retries)
|
return format_html('<a href="{}" onclick="return showAddAnotherPopup(this);">{}</a>', url, instance.retries)
|
||||||
display_retries.short_description = _("Retries")
|
display_retries.short_description = _("Retries")
|
||||||
display_retries.admin_order_field = 'retries'
|
display_retries.admin_order_field = 'retries'
|
||||||
display_retries.allow_tags = True
|
|
||||||
|
|
||||||
def display_content(self, instance):
|
def display_content(self, instance):
|
||||||
part = email.message_from_string(instance.content)
|
part = email.message_from_string(instance.content)
|
||||||
|
@ -99,9 +99,8 @@ class MessageAdmin(ExtendedModelAdmin):
|
||||||
payload = payload.decode(charset)
|
payload = payload.decode(charset)
|
||||||
if part.get_content_type() == 'text/plain':
|
if part.get_content_type() == 'text/plain':
|
||||||
payload = payload.replace('\n', '<br>').replace(' ', ' ')
|
payload = payload.replace('\n', '<br>').replace(' ', ' ')
|
||||||
return payload
|
return mark_safe(payload)
|
||||||
display_content.short_description = _("Content")
|
display_content.short_description = _("Content")
|
||||||
display_content.allow_tags = True
|
|
||||||
|
|
||||||
def display_full_subject(self, instance):
|
def display_full_subject(self, instance):
|
||||||
return instance.subject
|
return instance.subject
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ class Migration(migrations.Migration):
|
||||||
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
|
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
|
||||||
('date', models.DateTimeField(auto_now_add=True)),
|
('date', models.DateTimeField(auto_now_add=True)),
|
||||||
('log_message', models.TextField()),
|
('log_message', models.TextField()),
|
||||||
('message', models.ForeignKey(to='mailer.Message', editable=False, related_name='logs')),
|
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailer.Message', editable=False, related_name='logs')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('mailer', '0001_initial'), ('mailer', '0002_auto_20150617_1021'), ('mailer', '0003_auto_20150617_1024'), ('mailer', '0004_auto_20150805_1328'), ('mailer', '0005_auto_20160219_1056')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Message',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('state', models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failes')], default='QUEUED', max_length=16, verbose_name='State')),
|
||||||
|
('priority', models.PositiveIntegerField(choices=[(0, 'Critical (not queued)'), (1, 'High'), (2, 'Normal'), (3, 'Low')], default=2, verbose_name='Priority')),
|
||||||
|
('to_address', models.CharField(max_length=256)),
|
||||||
|
('from_address', models.CharField(max_length=256)),
|
||||||
|
('subject', models.CharField(max_length=256, verbose_name='subject')),
|
||||||
|
('content', models.TextField(verbose_name='content')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||||
|
('retries', models.PositiveIntegerField(default=0, verbose_name='retries')),
|
||||||
|
('last_retry', models.DateTimeField(auto_now=True, verbose_name='last try')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SMTPLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
|
||||||
|
('date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('log_message', models.TextField()),
|
||||||
|
('message', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='mailer.Message')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='message',
|
||||||
|
old_name='last_retry',
|
||||||
|
new_name='last_try',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='last_try',
|
||||||
|
field=models.DateTimeField(verbose_name='last try'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='subject',
|
||||||
|
field=models.TextField(verbose_name='subject'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='last_try',
|
||||||
|
field=models.DateTimeField(null=True, verbose_name='last try'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failed')], default='QUEUED', max_length=16, verbose_name='State'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='last_try',
|
||||||
|
field=models.DateTimeField(db_index=True, null=True, verbose_name='last try'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='priority',
|
||||||
|
field=models.PositiveIntegerField(choices=[(0, 'Critical (not queued)'), (1, 'High'), (2, 'Normal'), (3, 'Low')], db_index=True, default=2, verbose_name='Priority'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='retries',
|
||||||
|
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='retries'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failed')], db_index=True, default='QUEUED', max_length=16, verbose_name='State'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -67,7 +67,7 @@ class SMTPLog(models.Model):
|
||||||
(SUCCESS, _("Success")),
|
(SUCCESS, _("Success")),
|
||||||
(FAILURE, _("Failure")),
|
(FAILURE, _("Failure")),
|
||||||
)
|
)
|
||||||
message = models.ForeignKey(Message, editable=False, related_name='logs')
|
message = models.ForeignKey(Message, editable=False, related_name='logs', on_delete=models.CASCADE)
|
||||||
result = models.CharField(max_length=16, choices=RESULTS, default=SUCCESS)
|
result = models.CharField(max_length=16, choices=RESULTS, default=SUCCESS)
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
log_message = models.TextField()
|
log_message = models.TextField()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -38,15 +39,13 @@ class MiscServiceAdmin(ExtendedModelAdmin):
|
||||||
actions = (disable, enable)
|
actions = (disable, enable)
|
||||||
|
|
||||||
def display_name(self, misc):
|
def display_name(self, misc):
|
||||||
return '<span title="%s">%s</span>' % (misc.description, misc.name)
|
return format_html('<span title="{}">{}</span>', misc.description, misc.name)
|
||||||
display_name.short_description = _("name")
|
display_name.short_description = _("name")
|
||||||
display_name.allow_tags = True
|
|
||||||
display_name.admin_order_field = 'name'
|
display_name.admin_order_field = 'name'
|
||||||
|
|
||||||
def display_verbose_name(self, misc):
|
def display_verbose_name(self, misc):
|
||||||
return '<span title="%s">%s</span>' % (misc.description, misc.verbose_name)
|
return format_html('<span title="{}">{}</span>', misc.description, misc.verbose_name)
|
||||||
display_verbose_name.short_description = _("verbose name")
|
display_verbose_name.short_description = _("verbose name")
|
||||||
display_verbose_name.allow_tags = True
|
|
||||||
display_verbose_name.admin_order_field = 'verbose_name'
|
display_verbose_name.admin_order_field = 'verbose_name'
|
||||||
|
|
||||||
def num_instances(self, misc):
|
def num_instances(self, misc):
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
import django.db.models.deletion
|
||||||
import orchestra.core.validators
|
import orchestra.core.validators
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import orchestra.models.fields
|
import orchestra.models.fields
|
||||||
|
@ -22,7 +23,7 @@ class Migration(migrations.Migration):
|
||||||
('description', models.TextField(blank=True, verbose_name='description')),
|
('description', models.TextField(blank=True, verbose_name='description')),
|
||||||
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
|
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
|
||||||
('account', models.ForeignKey(related_name='miscellaneous', verbose_name='account', to=settings.AUTH_USER_MODEL)),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='miscellaneous', verbose_name='account', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'miscellaneous',
|
'verbose_name_plural': 'miscellaneous',
|
||||||
|
@ -43,6 +44,6 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='miscellaneous',
|
model_name='miscellaneous',
|
||||||
name='service',
|
name='service',
|
||||||
field=models.ForeignKey(related_name='instances', verbose_name='service', to='miscellaneous.MiscService'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', verbose_name='service', to='miscellaneous.MiscService'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2021-04-22 11:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('miscellaneous', '0001_initial'), ('miscellaneous', '0002_auto_20150723_1252')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Miscellaneous',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('identifier', orchestra.models.fields.NullableCharField(help_text='A unique identifier for this service.', max_length=256, null=True, unique=True, verbose_name='identifier')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='description')),
|
||||||
|
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='miscellaneous', to=settings.AUTH_USER_MODEL, verbose_name='account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'miscellaneous',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MiscService',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Raw name used for internal referenciation, i.e. service match definition', max_length=32, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
|
||||||
|
('verbose_name', models.CharField(blank=True, help_text='Human readable name', max_length=256, verbose_name='verbose name')),
|
||||||
|
('description', models.TextField(blank=True, help_text='Optional description', verbose_name='description')),
|
||||||
|
('has_identifier', models.BooleanField(default=True, help_text='Designates if this service has a <b>unique text</b> field that identifies it or not.', verbose_name='has identifier')),
|
||||||
|
('has_amount', models.BooleanField(default=False, help_text='Designates whether this service has <tt>amount</tt> property or not.', verbose_name='has amount')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Whether new instances of this service can be created or not. Unselect this instead of deleting services.', verbose_name='active')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='miscellaneous',
|
||||||
|
name='service',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='miscellaneous.MiscService', verbose_name='service'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='miscellaneous',
|
||||||
|
name='identifier',
|
||||||
|
field=orchestra.models.fields.NullableCharField(db_index=True, help_text='A unique identifier for this service.', max_length=256, null=True, unique=True, verbose_name='identifier'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -42,10 +42,10 @@ class MiscService(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Miscellaneous(models.Model):
|
class Miscellaneous(models.Model):
|
||||||
service = models.ForeignKey(MiscService, verbose_name=_("service"),
|
service = models.ForeignKey(MiscService, on_delete=models.CASCADE,
|
||||||
related_name='instances')
|
verbose_name=_("service"), related_name='instances')
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
|
||||||
related_name='miscellaneous')
|
verbose_name=_("account"), related_name='miscellaneous')
|
||||||
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
|
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
|
||||||
db_index=True, help_text=_("A unique identifier for this service."))
|
db_index=True, help_text=_("A unique identifier for this service."))
|
||||||
description = models.TextField(_("description"), blank=True)
|
description = models.TextField(_("description"), blank=True)
|
||||||
|
|
|
@ -39,10 +39,10 @@ class Operation():
|
||||||
self.routes = routes
|
self.routes = routes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, operations, serialize=False, async=None):
|
def execute(cls, operations, serialize=False, run_async=None):
|
||||||
from . import manager
|
from . import manager
|
||||||
scripts, backend_serialize = manager.generate(operations)
|
scripts, backend_serialize = manager.generate(operations)
|
||||||
return manager.execute(scripts, serialize=(serialize or backend_serialize), async=async)
|
return manager.execute(scripts, serialize=(serialize or backend_serialize), run_async=run_async)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_for_action(cls, instances, action):
|
def create_for_action(cls, instances, action):
|
||||||
|
|
|
@ -30,14 +30,14 @@ STATE_COLORS = {
|
||||||
|
|
||||||
class RouteAdmin(ExtendedModelAdmin):
|
class RouteAdmin(ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'async',
|
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'run_async',
|
||||||
'is_active'
|
'is_active'
|
||||||
)
|
)
|
||||||
list_editable = ('host', 'match', 'async', 'is_active')
|
list_editable = ('host', 'match', 'run_async', 'is_active')
|
||||||
list_filter = ('host', 'is_active', 'async', 'backend')
|
list_filter = ('host', 'is_active', 'run_async', 'backend')
|
||||||
list_prefetch_related = ('host',)
|
list_prefetch_related = ('host',)
|
||||||
ordering = ('backend',)
|
ordering = ('backend',)
|
||||||
add_fields = ('backend', 'host', 'match', 'async', 'is_active')
|
add_fields = ('backend', 'host', 'match', 'run_async', 'is_active')
|
||||||
change_form = RouteForm
|
change_form = RouteForm
|
||||||
actions = (orchestrate,)
|
actions = (orchestrate,)
|
||||||
change_view_actions = actions
|
change_view_actions = actions
|
||||||
|
@ -51,19 +51,18 @@ class RouteAdmin(ExtendedModelAdmin):
|
||||||
|
|
||||||
def display_model(self, route):
|
def display_model(self, route):
|
||||||
try:
|
try:
|
||||||
return escape(route.backend_class.model)
|
return route.backend_class.model
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
return mark_safe("<span style='color: red;'>NOT AVAILABLE</span>")
|
||||||
display_model.short_description = _("model")
|
display_model.short_description = _("model")
|
||||||
display_model.allow_tags = True
|
|
||||||
|
|
||||||
|
@mark_safe
|
||||||
def display_actions(self, route):
|
def display_actions(self, route):
|
||||||
try:
|
try:
|
||||||
return '<br>'.join(route.backend_class.get_actions())
|
return '<br>'.join(route.backend_class.get_actions())
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
return "<span style='color: red;'>NOT AVAILABLE</span>"
|
||||||
display_actions.short_description = _("actions")
|
display_actions.short_description = _("actions")
|
||||||
display_actions.allow_tags = True
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Provides dynamic help text on backend form field """
|
""" Provides dynamic help text on backend form field """
|
||||||
|
@ -120,7 +119,6 @@ class BackendOperationInline(admin.TabularInline):
|
||||||
return _("Deleted {0}").format(operation.instance_repr or '-'.join(
|
return _("Deleted {0}").format(operation.instance_repr or '-'.join(
|
||||||
(escape(operation.content_type), escape(operation.object_id))))
|
(escape(operation.content_type), escape(operation.object_id))))
|
||||||
return link
|
return link
|
||||||
instance_link.allow_tags = True
|
|
||||||
instance_link.short_description = _("Instance")
|
instance_link.short_description = _("Instance")
|
||||||
|
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
|
@ -179,14 +177,12 @@ class ServerAdmin(ExtendedModelAdmin):
|
||||||
change_view_actions = actions
|
change_view_actions = actions
|
||||||
|
|
||||||
def display_ping(self, instance):
|
def display_ping(self, instance):
|
||||||
return self._remote_state[instance.pk][0]
|
return mark_safe(self._remote_state[instance.pk][0])
|
||||||
display_ping.short_description = _("Ping")
|
display_ping.short_description = _("Ping")
|
||||||
display_ping.allow_tags = True
|
|
||||||
|
|
||||||
def display_uptime(self, instance):
|
def display_uptime(self, instance):
|
||||||
return self._remote_state[instance.pk][1]
|
return mark_safe(self._remote_state[instance.pk][1])
|
||||||
display_uptime.short_description = _("Uptime")
|
display_uptime.short_description = _("Uptime")
|
||||||
display_uptime.allow_tags = True
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue