Compare commits

..

52 Commits

Author SHA1 Message Date
Santiago L a169bc60b8 Use Python 3.4 on development Docker 2021-02-22 13:01:53 +01:00
Santiago L ebe8e95a75 Bump django to 1.11.29 (latest release of 1.11) 2021-02-22 12:58:25 +01:00
Santiago L da29e86860 Fix DEFAULT_FILTER_BACKENDS setting. 2021-02-19 13:10:00 +01:00
Santiago L 8079de4e76 Fix `deploy.sh` script. 2021-02-19 12:25:04 +01:00
Santiago L 9957b4ebeb Install lxml using apt. 2021-02-19 12:03:05 +01:00
Santiago L b6852348eb Fix requirements & restore devel Docker 2021-02-19 11:25:58 +01:00
Cayo Puigdefabregas b2f3bcc617 fixing version 2021-02-17 10:45:08 +01:00
Cayo Puigdefabregas c16d067bd8 new version of always_eager 2021-02-17 10:44:46 +01:00
Cayo Puigdefabregas 950e04df92 up the version of django-celery 2021-02-17 10:23:51 +01:00
Cayo Puigdefabregas 091530bfd4 adding CELERY_ALWAYS_EAGER to settings 2021-02-17 10:22:52 +01:00
Cayo Puigdefabregas 22b95b5b51 update some packages 2021-02-16 23:25:42 +01:00
Cayo Puigdefabregas e7c037ce72 adding vars for settings in execution time 2021-02-16 23:25:07 +01:00
Cayo Puigdefabregas 20e9c14524 fixing bug 2021-02-16 13:55:46 +01:00
Cayo Puigdefabregas d22aebf68a install and up services 2021-02-16 13:55:16 +01:00
Cayo Puigdefabregas 6e03d7bd54 update django-celery 2021-02-16 13:54:45 +01:00
Cayo Puigdefabregas fc74db4a76 proof than the problem is async 2021-02-15 21:13:47 +01:00
Cayo Puigdefabregas e5beae6360 comment skip of test_add 2021-02-15 21:12:53 +01:00
Cayo Puigdefabregas 57688426b6 adding improvements to init_project.sh 2021-02-15 21:11:24 +01:00
Cayo Puigdefabregas 1f57cdb48d modify session with selenium 2021-02-15 21:09:40 +01:00
Santiago L f87ad48b7c Create docker-compose devel environment 2021-02-12 19:13:11 +01:00
Cayo Puigdefabregas 481515363b fixing initial 2021-02-12 16:26:59 +01:00
Cayo Puigdefabregas e5ca77f018 fixing createdb 2021-02-12 16:18:51 +01:00
Cayo Puigdefabregas b05481c662 fixing initial 2021-02-12 15:08:16 +01:00
Cayo Puigdefabregas 2d1cd175ee adding files for deploy in docker 2021-02-12 14:58:37 +01:00
Santiago L 4536d651ec Store screenshots on project path. 2021-02-12 13:34:00 +01:00
Cayo Puigdefabregas a6f829e66c try login from selenium 2021-02-10 20:27:53 +01:00
Cayo Puigdefabregas 824bc7f8cd uncomment skip 2021-02-10 18:28:43 +01:00
Cayo Puigdefabregas 4286c4f77a get UNIXUserController instead of SystemUserBackend and RoundcubeIdentityController instead of PasswdVirtualUserBackend 2021-02-10 18:18:40 +01:00
Cayo Puigdefabregas 7bee5facbc adding coverage 2021-02-09 19:39:22 +01:00
Cayo Puigdefabregas 153f869f0d add one skip more 2021-02-09 18:24:27 +01:00
Cayo Puigdefabregas 88193de18a adding skip to tests 2021-02-09 18:16:51 +01:00
Cayo Puigdefabregas 3f5ed20926 fixing test traffic monitors is a list 2021-02-08 20:15:38 +01:00
Cayo Puigdefabregas 28001247d2 fixing test_traffic get_total 2021-02-08 19:39:53 +01:00
Cayo Puigdefabregas d6e94fbc5d fixing mailbox test 2021-02-08 19:24:40 +01:00
Cayo Puigdefabregas c6f8e2cf61 fixing job test 2021-02-08 19:24:16 +01:00
Cayo Puigdefabregas 9ebce376ec fixing ftp test 2021-02-08 19:23:53 +01:00
Cayo Puigdefabregas 8ad269357f fixing DomainBillingTest 2021-02-08 19:04:49 +01:00
Cayo Puigdefabregas 550c4db74e fixing test_route 2021-02-08 18:47:16 +01:00
Cayo Puigdefabregas 2497d31c49 fixed choices 2021-02-08 15:31:15 +01:00
Cayo Puigdefabregas cc1a2622c5 try fix gobject-introspection 2021-02-08 15:12:44 +01:00
Cayo Puigdefabregas 05a2c4078a try fix gobject-introspection 2021-02-08 15:05:10 +01:00
Cayo Puigdefabregas 13210c332e fixing orchestra-orm requirements 2021-02-08 10:32:24 +01:00
Cayo Puigdefabregas df9e413ece server slave different of master 2021-02-03 16:48:18 +01:00
Cayo Puigdefabregas af3f9058af clean 2021-02-03 16:47:24 +01:00
Cayo Puigdefabregas 25112f20ea fixed admin_login in test 2021-02-03 10:47:35 +01:00
Cayo Puigdefabregas 4ec22e4e36 fixed is_staff 2021-02-02 21:24:05 +01:00
Cayo Puigdefabregas 38329f84df fixed tests 2021-02-02 21:23:41 +01:00
Cayo Puigdefabregas e5b7f03347 fixed versions 2021-02-02 21:23:23 +01:00
Cayo Puigdefabregas e1cbc385d0 fixing requirements 2021-02-02 15:01:45 +01:00
Cayo Puigdefabregas d6d3aabd92 Fixed bug in python backend 2021-02-01 21:05:11 +01:00
Cayo Puigdefabregas 15d3c4feff add github workflow 2021-02-01 20:02:49 +01:00
Cayo Puigdefabregas 5df58ff3e6 add requirements 2021-02-01 20:01:59 +01:00
204 changed files with 2674 additions and 4169 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
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

76
.github/workflows/django.yml vendored Normal file
View File

@ -0,0 +1,76 @@
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 }}

View File

@ -0,0 +1,70 @@
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.

41
examples/Dockerfile Normal file
View File

@ -0,0 +1,41 @@
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

33
examples/README.md Normal file
View File

@ -0,0 +1,33 @@
# 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
```

4
examples/createdb.sql Normal file
View File

@ -0,0 +1,4 @@
create database orchestra;
CREATE USER orchestra WITH PASSWORD 'orchestra';
GRANT ALL PRIVILEGES ON DATABASE orchestra TO orchestra;
ALTER ROLE orchestra CREATEDB;

View File

@ -0,0 +1,17 @@
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

5
examples/env.example Normal file
View File

@ -0,0 +1,5 @@
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

27
examples/init_project.sh Normal file
View File

@ -0,0 +1,27 @@
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

53
examples/requirements.txt Normal file
View File

@ -0,0 +1,53 @@
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

View File

@ -93,7 +93,7 @@ Remember create a database for your project and give permitions for the correct
```
psql -U postgres
psql (12.4)
Digite «help» para obtener ayuda.
Digite «help».
postgres=# CREATE database orchesta;
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';

View File

@ -3,7 +3,7 @@ from collections import OrderedDict
from functools import update_wrapper
from django.contrib import admin
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _

View File

@ -1,4 +1,4 @@
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from fluent_dashboard import dashboard, appsettings
from fluent_dashboard.modules import CmsAppIconList

View File

@ -5,7 +5,7 @@ from django import forms
from django.contrib.admin import helpers
from django.core import validators
from django.forms.models import modelformset_factory, BaseModelFormSet
from django.template import Template
from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _
from orchestra.forms.widgets import SpanWidget
@ -28,9 +28,9 @@ class AdminFormMixin(object):
' {% include "admin/includes/fieldset.html" %}'
'{% endfor %}'
)
context = {
context = Context({
'adminform': adminform
}
})
return template.render(context)
@ -71,9 +71,9 @@ class AdminFormSet(BaseModelFormSet):
</div>
</div>""")
)
context = {
context = Context({
'formset': self
}
})
return template.render(context)

View File

@ -1,7 +1,7 @@
from copy import deepcopy
from admin_tools.menu import items, Menu
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _

View File

@ -6,11 +6,11 @@ from functools import wraps
from django.conf import settings
from django.contrib import admin
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse, NoReverseMatch
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db import models
from django.shortcuts import redirect
from django.utils import timezone
from django.utils.html import escape, format_html
from django.utils.html import escape
from django.utils.safestring import mark_safe
from orchestra.models.utils import get_field_value
@ -113,21 +113,21 @@ def admin_link(*args, **kwargs):
return '---'
if not getattr(obj, 'pk', None):
return '---'
display_ = kwargs.get('display')
if display_:
display_ = getattr(obj, display_, display_)
display = kwargs.get('display')
if display:
display = getattr(obj, display, display)
else:
display_ = obj
display = obj
try:
url = change_url(obj)
except NoReverseMatch:
# Does not has admin
return str(display_)
return str(display)
extra = ''
if kwargs['popup']:
extra = mark_safe('onclick="return showAddAnotherPopup(this);"')
extra = 'onclick="return showAddAnotherPopup(this);"'
title = "Change %s" % obj._meta.verbose_name
return format_html('<a href="{}" title="{}" {}>{}</a>', url, title, extra, display_)
return mark_safe('<a href="%s" title="%s" %s>%s</a>' % (url, title, extra, display))
@admin_field
@ -158,7 +158,7 @@ def admin_date(*args, **kwargs):
date = date.strftime("%Y-%m-%d %H:%M:%S %Z")
else:
date = date.strftime("%Y-%m-%d")
return format_html('<span title="{0}">{1}</span>', date, natural)
return '<span title="{0}">{1}</span>'.format(date, escape(natural))
def get_object_from_url(modeladmin, request):

View File

@ -1,12 +1,12 @@
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from .serializers import SetPasswordSerializer
class SetPasswordApiMixin(object):
@action(detail=True, methods=['post'], serializer_class=SetPasswordSerializer)
@detail_route(methods=['post'], serializer_class=SetPasswordSerializer)
def set_password(self, request, pk):
obj = self.get_object()
data = request.data

View File

@ -1,4 +1,4 @@
from django.urls import NoReverseMatch
from django.core.urlresolvers import NoReverseMatch
from rest_framework.reverse import reverse
@ -23,16 +23,16 @@ def link_wrap(view, view_names):
return wrapper
def insert_links(viewset, basename):
collection_links = ['api-root', '%s-list' % basename]
object_links = ['api-root', '%s-list' % basename, '%s-detail' % basename]
def insert_links(viewset, base_name):
collection_links = ['api-root', '%s-list' % base_name]
object_links = ['api-root', '%s-list' % base_name, '%s-detail' % base_name]
exception_links = ['api-root']
list_links = ['api-root']
retrieve_links = ['api-root', '%s-list' % basename]
retrieve_links = ['api-root', '%s-list' % base_name]
# Determine any `@action` or `@link` decorated methods on the viewset
for methodname in dir(viewset):
method = getattr(viewset, methodname)
view_name = '%s-%s' % (basename, methodname.replace('_', '-'))
view_name = '%s-%s' % (base_name, methodname.replace('_', '-'))
if hasattr(method, 'collection_bind_to_methods'):
list_links.append(view_name)
retrieve_links.append(view_name)

View File

@ -65,12 +65,12 @@ class LinkHeaderRouter(DefaultRouter):
APIRoot.router = self
return APIRoot.as_view()
def register(self, prefix, viewset, basename=None):
def register(self, prefix, viewset, base_name=None):
""" inserts link headers on every viewset """
if basename is None:
basename = self.get_default_basename(viewset)
insert_links(viewset, basename)
self.registry.append((prefix, viewset, basename))
if base_name is None:
base_name = self.get_default_base_name(viewset)
insert_links(viewset, base_name)
self.registry.append((prefix, viewset, base_name))
def get_viewset(self, prefix_or_model):
for _prefix, viewset, __ in self.registry:

View File

@ -23,7 +23,7 @@ class APIRoot(views.APIView):
'accountancy': {},
'services': {},
}
if not request.user.is_anonymous:
if not request.user.is_anonymous():
list_name = '{basename}-list'
detail_name = '{basename}-detail'
for prefix, viewset, basename in self.router.registry:

View File

@ -64,10 +64,7 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer):
""" returns object on to_internal_value based on URL """
def to_internal_value(self, data):
try:
url = data.get('url')
except AttributeError:
url = None
url = data.get('url')
if not url:
raise ValidationError({
'url': "URL is required."
@ -84,14 +81,14 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
validators=[validate_password], write_only=True, required=False,
style={'widget': widgets.PasswordInput})
def validate_password(self, value):
def validate_password(self, attrs, source):
""" POST only password """
if self.instance:
if value:
if 'password' in attrs:
raise serializers.ValidationError(_("Can not set password"))
elif not value:
elif 'password' not in attrs:
raise serializers.ValidationError(_("Password required"))
return value
return attrs
def validate(self, attrs):
""" remove password in case is not a real model field """

View File

@ -116,20 +116,17 @@ function install_requirements () {
update-locale LANG=en_US.UTF-8
fi
# lxml: libxml2-dev, libxslt1-dev, zlib1g-dev
APT="bind9utils \
ca-certificates \
gettext \
libcrack2-dev \
libxml2-dev \
libxslt1-dev \
python3 \
python3-pip \
python3-dev \
python3-lxml \
ssh-client \
wget \
xvfb \
zlib1g-dev"
xvfb"
if $testing; then
APT="${APT} \
git \
@ -150,14 +147,13 @@ function install_requirements () {
fi
# cracklib and lxml are excluded on the requirements.txt because they need unconvinient system dependencies
PIP="$(wget http://git.io/orchestra-requirements.txt -O - | tr '\n' ' ') \
cracklib \
lxml==3.3.5"
PIP="$(wget https://raw.githubusercontent.com/ribaguifi/django-orchestra/dev/github-actions/requirements.txt -O - | tr '\n' ' ') \
cracklib"
if $testing; then
PIP="${PIP} \
selenium \
xvfbwrapper \
freezegun==0.3.14 \
freezegun \
coverage \
flake8 \
django-debug-toolbar==1.3.0 \

View File

@ -178,7 +178,7 @@ def fire_pending_tasks(manage, db):
if is_due(now, minute, hour, day_of_week, day_of_month, month_of_year):
command = 'python3 -W ignore::DeprecationWarning {manage} runtask {task_id}'.format(
manage=manage, task_id=task_id)
proc = run(command, run_async=True)
proc = run(command, async=True)
yield proc
@ -201,7 +201,7 @@ def fire_pending_messages(settings, db):
if has_pending_messages(settings, db):
command = 'python3 -W ignore::DeprecationWarning {manage} sendpendingmessages'.format(manage=manage)
proc = run(command, run_async=True)
proc = run(command, async=True)
yield proc

View File

@ -25,7 +25,6 @@ SECRET_KEY = '{{ secret_key }}'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
@ -66,7 +65,6 @@ INSTALLED_APPS = [
'admin_tools.dashboard',
'rest_framework',
'rest_framework.authtoken',
'django_filters',
'passlib.ext.django',
'django_countries',
# 'debug_toolbar',
@ -86,21 +84,6 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'orchestra.core.caches.RequestCacheMiddleware',
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
]
ROOT_URLCONF = '{{ project_name }}.urls'
TEMPLATES = [
@ -144,24 +127,6 @@ DATABASES = {
}
# Password validation
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
@ -203,6 +168,22 @@ LOCALE_PATHS = (
ORCHESTRA_SITE_NAME = '{{ project_name }}'
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
# 'django.middleware.locale.LocaleMiddleware'
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'orchestra.core.caches.RequestCacheMiddleware',
# also handles transations, ATOMIC_REQUESTS does not wrap middlewares
'orchestra.contrib.orchestration.middlewares.OperationsMiddleware',
)
AUTH_USER_MODEL = 'accounts.Account'
@ -261,6 +242,7 @@ PASSLIB_CONFIG = (
"default = sha512_crypt\n"
"deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, "
" django_des_crypt, des_crypt, hex_md5\n"
"all__vary_rounds = 0.05\n"
"django_pbkdf2_sha256__min_rounds = 10000\n"
"sha512_crypt__min_rounds = 80000\n"
"staff__django_pbkdf2_sha256__default_rounds = 12500\n"

View File

View File

@ -0,0 +1,13 @@
#!/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)

View File

View File

@ -0,0 +1,261 @@
"""
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',)),
)

View File

@ -0,0 +1,6 @@
from django.conf.urls import include, url
urlpatterns = [
url(r'', include('orchestra.urls')),
]

View File

@ -0,0 +1,14 @@
"""
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()

View File

@ -4,7 +4,7 @@ from django.contrib import messages
from django.contrib.admin import helpers
from django.contrib.admin.utils import NestedObjects, quote
from django.contrib.auth import get_permission_codename
from django.urls import reverse, NoReverseMatch
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db import router
from django.shortcuts import redirect, render
from django.template.response import TemplateResponse
@ -175,7 +175,7 @@ def delete_related_services(modeladmin, request, queryset):
for model, objs in collector.model_objs.items():
count = 0
# discount main systemuser
if model is modeladmin.model.main_systemuser.field.related_model:
if model is modeladmin.model.main_systemuser.field.rel.to:
count = len(objs) - 1
# Discount account
elif model is not modeladmin.model and model in registered_services:

View File

@ -8,7 +8,7 @@ from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin.utils import unquote
from django.contrib.auth import admin as auth
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.templatetags.static import static
from django.utils.safestring import mark_safe
@ -158,7 +158,6 @@ class AccountListAdmin(AccountAdmin):
actions = None
change_list_template = 'admin/accounts/account/select_account_list.html'
@mark_safe
def select_account(self, instance):
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
context = {
@ -168,6 +167,7 @@ class AccountListAdmin(AccountAdmin):
}
return _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % context
select_account.short_description = _("account")
select_account.allow_tags = True
select_account.admin_order_field = 'username'
def changelist_view(self, request, extra_context=None):
@ -207,7 +207,6 @@ class AccountAdminMixin(object):
account = None
list_select_related = ('account',)
@mark_safe
def display_active(self, instance):
if not instance.is_active:
return '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
@ -216,12 +215,14 @@ class AccountAdminMixin(object):
return '<img style="width:13px" src="%s" alt="False" title="%s">' % (static('admin/img/inline-delete.svg'), msg)
return '<img src="%s" alt="False">' % static('admin/img/icon-yes.svg')
display_active.short_description = _("active")
display_active.allow_tags = True
display_active.admin_order_field = 'is_active'
def account_link(self, instance):
account = instance.account if instance.pk else self.account
return admin_link()(account)
account_link.short_description = _("account")
account_link.allow_tags = True
account_link.admin_order_field = 'account__username'
def get_form(self, request, obj=None, **kwargs):

View File

@ -47,7 +47,7 @@ def create_account_creation_form():
# Previous validation error
return
errors = {}
systemuser_model = Account.main_systemuser.field.related_model
systemuser_model = Account.main_systemuser.field.rel.to
if systemuser_model.objects.filter(username=account.username).exists():
errors['username'] = _("A system user with this name already exists.")
for model, key, related_kwargs, __ in create_related:

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import django.contrib.auth.models
@ -33,7 +32,7 @@ class Migration(migrations.Migration):
('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')),
('is_active', models.BooleanField(help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')),
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main')),
('main_systemuser', models.ForeignKey(to='systemusers.SystemUser', editable=False, null=True, related_name='accounts_main')),
],
options={
'abstract': False,

View File

@ -1,86 +0,0 @@
# -*- 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'),
),
]

View File

@ -1,25 +0,0 @@
# -*- 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'),
),
]

View File

@ -29,7 +29,7 @@ class Account(auth.AbstractBaseUser):
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
])
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
related_name='accounts_main', editable=False, on_delete=models.SET_NULL)
related_name='accounts_main', editable=False)
short_name = models.CharField(_("short name"), max_length=64, blank=True)
full_name = models.CharField(_("full name"), max_length=256)
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
@ -53,7 +53,6 @@ class Account(auth.AbstractBaseUser):
REQUIRED_FIELDS = ['email']
def __init__(self, *args, **kwargs):
# ignore `is_staff` kwarg because is handled with `is_superuser`
kwargs.pop('is_staff', None)
super().__init__(*args, **kwargs)

View File

@ -5,7 +5,7 @@ from datetime import date
from django.contrib import messages
from django.contrib.admin import helpers
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db import transaction
from django.forms.models import modelformset_factory
from django.http import HttpResponse, HttpResponseRedirect

View File

@ -2,12 +2,11 @@ from django import forms
from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin.utils import unquote
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import F, Sum, Prefetch
from django.db.models.functions import Coalesce
from django.templatetags.static import static
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import redirect
@ -16,7 +15,7 @@ from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from . import settings, actions
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
@ -68,7 +67,6 @@ class BillLineInline(admin.TabularInline):
order_link = admin_link('order', display='pk')
@mark_safe
def display_total(self, line):
if line.pk:
total = line.compute_total()
@ -80,6 +78,7 @@ class BillLineInline(admin.TabularInline):
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
return '<a href="%s">%s</a>' % (url, total)
display_total.short_description = _("Total")
display_total.allow_tags = True
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
@ -105,26 +104,27 @@ class ClosedBillLineInline(BillLineInline):
readonly_fields = fields
can_delete = False
@mark_safe
def display_description(self, line):
descriptions = [line.description]
for subline in line.sublines.all():
descriptions.append('&nbsp;' * 4 + subline.description)
descriptions.append('&nbsp;'*4+subline.description)
return '<br>'.join(descriptions)
display_description.short_description = _("Description")
display_description.allow_tags = True
@mark_safe
def display_subtotal(self, line):
subtotals = ['&nbsp;' + str(line.subtotal)]
for subline in line.sublines.all():
subtotals.append(str(subline.total))
return '<br>'.join(subtotals)
display_subtotal.short_description = _("Subtotal")
display_subtotal.allow_tags = True
def display_total(self, line):
if line.pk:
return line.compute_total()
display_total.short_description = _("Total")
display_total.allow_tags = True
def has_add_permission(self, request):
return False
@ -242,7 +242,6 @@ class BillLineManagerAdmin(BillLineAdmin):
class BillAdminMixin(AccountAdminMixin):
@mark_safe
def display_total_with_subtotals(self, bill):
if bill.pk:
currency = settings.BILLS_CURRENCY.lower()
@ -252,10 +251,10 @@ class BillAdminMixin(AccountAdminMixin):
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
subtotals = '\n'.join(subtotals)
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
display_total_with_subtotals.allow_tags = True
display_total_with_subtotals.short_description = _("total")
display_total_with_subtotals.admin_order_field = 'approx_total'
@mark_safe
def display_payment_state(self, bill):
if bill.pk:
t_opts = bill.transactions.model._meta
@ -277,6 +276,7 @@ class BillAdminMixin(AccountAdminMixin):
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
url=url, color=color, name=state, title=title)
display_payment_state.allow_tags = True
display_payment_state.short_description = _("Payment")
def get_queryset(self, request):
@ -376,14 +376,16 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
def display_total(self, bill):
currency = settings.BILLS_CURRENCY.lower()
return format_html('{} &{};', bill.compute_total(), currency)
return '%s &%s;' % (bill.compute_total(), currency)
display_total.allow_tags = True
display_total.short_description = _("total")
display_total.admin_order_field = 'approx_total'
def type_link(self, bill):
bill_type = bill.type.lower()
url = reverse('admin:bills_%s_changelist' % bill_type)
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
type_link.allow_tags = True
type_link.short_description = _("type")
type_link.admin_order_field = 'type'
@ -477,7 +479,7 @@ class BillContactInline(admin.StackedInline):
if db_field.name == 'address':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
if db_field.name == 'email_usage':
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
return super().formfield_for_dbfield(db_field, **kwargs)

View File

@ -1,6 +1,6 @@
from django.http import HttpResponse
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from orchestra.api import router, LogApiMixin
from orchestra.contrib.accounts.api import AccountApiMixin
@ -15,7 +15,7 @@ class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
queryset = Bill.objects.all()
serializer_class = BillSerializer
@action(detail=True, methods=['get'])
@detail_route(methods=['get'])
def document(self, request, pk):
bill = self.get_object()
content_type = request.META.get('HTTP_ACCEPT')

View File

@ -1,5 +1,5 @@
from django.contrib.admin import SimpleListFilter
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _

View File

@ -1,5 +1,5 @@
from django.contrib import messages
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
@ -21,7 +21,7 @@ def validate_contact(request, bill, error=True):
message = msg.format(relation=_("Related"), account=account, url=url)
send(request, mark_safe(message))
valid = False
main = type(bill).account.field.related_model.objects.get_main()
main = type(bill).account.field.rel.to.objects.get_main()
if not hasattr(main, 'billcontact'):
account = force_text(main)
url = reverse('admin:accounts_account_change', args=(main.id,))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import models, migrations
@ -15,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='bill',
name='amend_of',
field=models.ForeignKey(to='bills.Bill', blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='amends', verbose_name='amend of', null=True),
field=models.ForeignKey(to='bills.Bill', blank=True, related_name='amends', verbose_name='amend of', null=True),
),
migrations.AlterField(
model_name='billcontact',

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
import datetime
from dateutil.relativedelta import relativedelta
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.core.validators import ValidationError, RegexValidator
from django.db import models
from django.db.models import F, Sum
from django.db.models.functions import Coalesce
from django.template import loader
from django.template import loader, Context
from django.utils import timezone, translation
from django.utils.encoding import force_text
from django.utils.functional import cached_property
@ -24,7 +24,7 @@ from . import settings
class BillContact(models.Model):
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
related_name='billcontact', on_delete=models.CASCADE)
related_name='billcontact')
name = models.CharField(_("name"), max_length=256, blank=True,
help_text=_("Account full name will be used when left blank."))
address = models.TextField(_("address"))
@ -102,9 +102,9 @@ class Bill(models.Model):
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s', on_delete=models.CASCADE)
related_name='%(class)s')
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
related_name='amends', on_delete=models.SET_NULL)
related_name='amends')
type = models.CharField(_("type"), max_length=16, choices=TYPES)
created_on = models.DateField(_("created on"), auto_now_add=True)
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
@ -303,7 +303,7 @@ class Bill(models.Model):
with translation.override(language or self.account.language):
if payment is False:
payment = self.account.paymentsources.get_default()
context = {
context = Context({
'bill': self,
'lines': self.lines.all().prefetch_related('sublines'),
'seller': self.seller,
@ -318,7 +318,7 @@ class Bill(models.Model):
'payment': payment and payment.get_bill_context(),
'default_due_date': self.get_due_date(payment=payment),
'now': timezone.now(),
}
})
template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
bill_template = loader.get_template(template)
@ -416,7 +416,7 @@ class ProForma(Bill):
class BillLine(models.Model):
""" Base model for bill item representation """
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines', on_delete=models.CASCADE)
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
description = models.CharField(_("description"), max_length=256)
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
@ -434,7 +434,7 @@ class BillLine(models.Model):
created_on = models.DateField(_("created"), auto_now_add=True)
# Amendment
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True, on_delete=models.CASCADE)
related_name='amendment_lines', null=True, blank=True)
class Meta:
get_latest_by = 'id'
@ -495,7 +495,7 @@ class BillSubline(models.Model):
)
# TODO: order info for undoing
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines', on_delete=models.CASCADE)
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
description = models.CharField(_("description"), max_length=256)
total = models.DecimalField(max_digits=12, decimal_places=2)
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)

View File

@ -7,7 +7,7 @@ from orchestra.admin.actions import SendEmail
from orchestra.admin.utils import insertattr, change_url
from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin
from orchestra.forms.widgets import PaddingCheckboxSelectMultiple
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from .filters import EmailUsageListFilter
from .models import Contact
@ -72,7 +72,7 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin):
if db_field.name == 'address':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
if db_field.name == 'email_usage':
kwargs['widget'] = PaddingCheckboxSelectMultiple(130)
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
@ -101,7 +101,7 @@ class ContactInline(admin.StackedInline):
if db_field.name == 'address':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
if db_field.name == 'email_usage':
kwargs['widget'] = PaddingCheckboxSelectMultiple(45)
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -33,7 +33,7 @@ class Contact(models.Model):
objects = ContactQuerySet.as_manager()
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='contacts', null=True, on_delete=models.SET_NULL)
related_name='contacts', null=True)
short_name = models.CharField(_("short name"), max_length=128)
full_name = models.CharField(_("full name"), max_length=256, blank=True)
email = models.EmailField()

View File

@ -1,8 +1,6 @@
from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
@ -52,14 +50,14 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_prefetch_related = ('users',)
actions = (list_accounts, save_selected)
@mark_safe
def display_users(self, db):
links = []
for user in db.users.all():
link = format_html('<a href="{}">{}</a>', change_url(user), user.username)
link = '<a href="%s">%s</a>' % (change_url(user), user.username)
links.append(link)
return '<br>'.join(links)
display_users.short_description = _("Users")
display_users.allow_tags = True
display_users.admin_order_field = 'users__username'
def save_model(self, request, obj, form, change):
@ -101,14 +99,14 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
list_prefetch_related = ('databases',)
actions = (list_accounts, save_selected)
@mark_safe
def display_databases(self, user):
links = []
for db in user.databases.all():
link = format_html('<a href="{}">{}</a>', change_url(db), db.name)
link = '<a href="%s">%s</a>' % (change_url(db), db.name)
links.append(link)
return '<br>'.join(links)
display_databases.short_description = _("Databases")
display_databases.allow_tags = True
display_databases.admin_order_field = 'databases__name'
def get_urls(self):

View File

@ -91,7 +91,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
class ReadOnlyPasswordHashWidget(forms.Widget):
def render(self, name, value, attrs, renderer=None):
def render(self, name, value, attrs):
original = ReadOnlyPasswordHashField.widget().render(name, value, attrs)
if 'Invalid' not in original:
return original

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import django.db.models.deletion
import orchestra.core.validators
@ -20,7 +19,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
('name', models.CharField(verbose_name='name', max_length=64, validators=[orchestra.core.validators.validate_name])),
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
('account', models.ForeignKey(related_name='databases', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
@ -30,7 +29,7 @@ class Migration(migrations.Migration):
('username', models.CharField(verbose_name='username', max_length=16, validators=[orchestra.core.validators.validate_name])),
('password', models.CharField(verbose_name='password', max_length=256)),
('type', models.CharField(default='mysql', choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], verbose_name='type', max_length=32)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
('account', models.ForeignKey(related_name='databaseusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'DB users',

View File

@ -1,82 +0,0 @@
# -*- 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'),
),
]

View File

@ -1,30 +0,0 @@
# -*- 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'),
),
]

View File

@ -20,8 +20,8 @@ class Database(models.Model):
type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES,
default=settings.DATABASES_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("Account"), related_name='databases')
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='databases')
comments = models.TextField(default="", blank=True)
class Meta:
@ -60,8 +60,8 @@ class DatabaseUser(models.Model):
type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES,
default=settings.DATABASES_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("Account"), related_name='databaseusers')
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='databaseusers')
class Meta:
verbose_name_plural = _("DB users")

View File

@ -1,24 +1,22 @@
import os
import socket
import time
import unittest
import MySQLdb
from unittest import skip
from django.conf import settings as djsettings
from django.core.management.base import CommandError
from django.urls import reverse
from orchestra.admin.utils import change_url
from orchestra.contrib.orchestration.models import Route, Server
from orchestra.utils.sys import sshrun
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
save_response_on_error, snapshot_on_error)
from django.core.urlresolvers import reverse
from selenium.webdriver.support.select import Select
from orchestra.admin.utils import change_url
from orchestra.contrib.orchestration.models import Server, Route
from orchestra.utils.sys import sshrun
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error,
snapshot_on_error)
from ... import backends, settings
from ...models import Database, DatabaseUser
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
class DatabaseTestMixin(object):
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
@ -53,6 +51,7 @@ class DatabaseTestMixin(object):
def add_group(self, username, groupname):
raise NotImplementedError
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_add(self):
dbname = '%s_database' % random_ascii(5)
username = '%s_dbuser' % random_ascii(5)
@ -60,6 +59,7 @@ class DatabaseTestMixin(object):
self.add(dbname, username, password)
self.validate_create_table(dbname, username, password)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_delete(self):
dbname = '%s_database' % random_ascii(5)
username = '%s_dbuser' % random_ascii(5)
@ -71,6 +71,7 @@ class DatabaseTestMixin(object):
self.validate_delete(dbname, username, password)
self.validate_delete_user(dbname, username)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_change_password(self):
dbname = '%s_database' % random_ascii(5)
username = '%s_dbuser' % random_ascii(5)
@ -84,6 +85,7 @@ class DatabaseTestMixin(object):
self.validate_login_error(dbname, username, password)
self.validate_create_table(dbname, username, new_password)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_add_user(self):
dbname = '%s_database' % random_ascii(5)
username = '%s_dbuser' % random_ascii(5)
@ -101,6 +103,7 @@ class DatabaseTestMixin(object):
self.validate_create_table(dbname, username, password)
self.validate_create_table(dbname, username2, password2)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_delete_user(self):
dbname = '%s_database' % random_ascii(5)
username = '%s_dbuser' % random_ascii(5)
@ -120,6 +123,7 @@ class DatabaseTestMixin(object):
self.validate_login_error(dbname, username2, password2)
self.validate_delete_user(username2, password2)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_swap_user(self):
dbname = '%s_database' % random_ascii(5)
username = '%s_dbuser' % random_ascii(5)
@ -183,7 +187,6 @@ class MySQLControllerMixin(object):
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
@unittest.skipUnless(TEST_REST_API, "REST API tests")
class RESTDatabaseMixin(DatabaseTestMixin):
def setUp(self):
super(RESTDatabaseMixin, self).setUp()

View File

@ -1,10 +1,8 @@
from django.contrib import admin
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models.functions import Concat, Coalesce
from django.templatetags.static import static
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
@ -74,8 +72,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
def structured_name(self, domain):
if domain.is_top:
return domain.name
return mark_safe('&nbsp;'*4 + domain.name)
return '&nbsp;'*4 + domain.name
structured_name.short_description = _("name")
structured_name.allow_tags = True
structured_name.admin_order_field = 'structured_name'
def display_is_top(self, domain):
@ -84,7 +83,6 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
display_is_top.boolean = True
display_is_top.admin_order_field = 'top'
@mark_safe
def display_websites(self, domain):
if apps.isinstalled('orchestra.contrib.websites'):
websites = domain.websites.all()
@ -94,22 +92,22 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
site_link = get_on_site_link(website.get_absolute_url())
admin_url = change_url(website)
title = _("Edit website")
link = format_html('<a href="{}" title="{}">{} {}</a>',
link = '<a href="%s" title="%s">%s %s</a>' % (
admin_url, title, website.name, site_link)
links.append(link)
return '<br>'.join(links)
add_url = reverse('admin:websites_website_add')
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
add_link = format_html(
'<a href="{}" title="{}"><img src="{}" /></a>', add_url,
_("Add website"), static('orchestra/images/add.png'),
image = '<img src="%s"></img>' % static('orchestra/images/add.png')
add_link = '<a href="%s" title="%s">%s</a>' % (
add_url, _("Add website"), image
)
return _("No website %s") % (add_link)
return '---'
display_websites.admin_order_field = 'websites__name'
display_websites.short_description = _("Websites")
display_websites.allow_tags = True
@mark_safe
def display_addresses(self, domain):
if apps.isinstalled('orchestra.contrib.mailboxes'):
add_url = reverse('admin:mailboxes_address_add')
@ -128,9 +126,10 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
return '---'
display_addresses.short_description = _("Addresses")
display_addresses.admin_order_field = 'addresses__count'
display_addresses.allow_tags = True
@mark_safe
def implicit_records(self, domain):
defaults = []
types = set(domain.records.values_list('type', flat=True))
ttl = settings.DOMAINS_DEFAULT_TTL
lines = []
@ -142,13 +141,14 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
value=record.value
)
if not domain.record_is_implicit(record, types):
line = format_html('<strike>{}</strike>', line)
line = '<strike>%s</strike>' % line
if record.type is Record.SOA:
lines.insert(0, line)
else:
lines.append(line)
return '<br>'.join(lines)
implicit_records.short_description = _("Implicit records")
implicit_records.allow_tags = True
def get_fieldsets(self, request, obj=None):
""" Add SOA fields when domain is top """

View File

@ -1,5 +1,5 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from orchestra.api import router
@ -19,7 +19,7 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
qs = super(DomainViewSet, self).get_queryset()
return qs.prefetch_related('records')
@action(detail=True)
@detail_route()
def view_zone(self, request, pk=None):
domain = self.get_object()
return Response({

View File

@ -2,7 +2,6 @@
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
import orchestra.contrib.domains.utils
import orchestra.contrib.domains.validators
from django.conf import settings
@ -21,8 +20,8 @@ class Migration(migrations.Migration):
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
('name', models.CharField(unique=True, max_length=256, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name', help_text='Domain or subdomain name.')),
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, verbose_name='serial', help_text='Serial number')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
('top', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
('account', models.ForeignKey(related_name='domains', help_text='Automatically selected for subdomains.', to=settings.AUTH_USER_MODEL, verbose_name='Account', blank=True)),
('top', models.ForeignKey(null=True, to='domains.Domain', editable=False, related_name='subdomain_set')),
],
),
migrations.CreateModel(
@ -32,7 +31,7 @@ class Migration(migrations.Migration):
('ttl', models.CharField(help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL', blank=True)),
('type', models.CharField(max_length=32, verbose_name='type', choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SOA', 'SOA')])),
('value', models.CharField(max_length=256, verbose_name='value')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
('domain', models.ForeignKey(related_name='records', to='domains.Domain', verbose_name='domain')),
],
),
]

View File

@ -1,83 +0,0 @@
# -*- 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),
),
]

View File

@ -2,7 +2,6 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import orchestra.contrib.domains.validators
@ -21,7 +20,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='domain',
name='top',
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),
field=models.ForeignKey(editable=False, verbose_name='top domain', related_name='subdomain_set', to='domains.Domain', null=True),
),
migrations.AlterField(
model_name='record',

View File

@ -1,26 +0,0 @@
# -*- 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'),
),
]

View File

@ -31,9 +31,9 @@ class Domain(models.Model):
validators.validate_allowed_domain
])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
related_name='domains', on_delete=models.CASCADE, help_text=_("Automatically selected for subdomains."))
related_name='domains', help_text=_("Automatically selected for subdomains."))
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
editable=False, verbose_name=_("top domain"), on_delete=models.CASCADE)
editable=False, verbose_name=_("top domain"))
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
help_text=_("A revision number that changes whenever this domain is updated."))
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
@ -318,7 +318,7 @@ class Record(models.Model):
SOA: (validators.validate_soa_record,),
}
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE)
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
validators=[validators.validate_zone_interval])

View File

@ -2,9 +2,10 @@ import os
import time
import socket
from functools import partial
from unittest import skip
from django.conf import settings as djsettings
from django.urls import reverse
from django.core.urlresolvers import reverse
from selenium.webdriver.support.select import Select
from orchestra.contrib.orchestration.models import Server, Route
@ -20,9 +21,9 @@ run = partial(run, display=False)
class DomainTestMixin(object):
MASTER_SERVER = os.environ.get('ORCHESTRA_MASTER_SERVER', 'localhost')
SLAVE_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
SLAVE_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost2')
MASTER_SERVER_ADDR = socket.gethostbyname(MASTER_SERVER)
SLAVE_SERVER_ADDR = socket.gethostbyname(SLAVE_SERVER)
SLAVE_SERVER_ADDR = '127.0.0.2'
def setUp(self):
djsettings.DEBUG = True
@ -176,6 +177,7 @@ class DomainTestMixin(object):
self.assertEqual('CNAME', cname[3])
self.assertEqual('external.server.org.', cname[4])
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_add(self):
self.add(self.ns1_name, self.ns1_records)
self.add(self.ns2_name, self.ns2_records)
@ -185,6 +187,7 @@ class DomainTestMixin(object):
time.sleep(1)
self.validate_add(self.SLAVE_SERVER_ADDR, self.domain_name)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_delete(self):
self.add(self.ns1_name, self.ns1_records)
self.add(self.ns2_name, self.ns2_records)
@ -194,6 +197,7 @@ class DomainTestMixin(object):
self.validate_delete(self.MASTER_SERVER_ADDR, name)
self.validate_delete(self.SLAVE_SERVER_ADDR, name)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_update(self):
self.add(self.ns1_name, self.ns1_records)
self.add(self.ns2_name, self.ns2_records)
@ -210,6 +214,7 @@ class DomainTestMixin(object):
time.sleep(5)
self.validate_www_update(self.SLAVE_SERVER_ADDR, self.domain_name)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_add_add_delete_delete(self):
self.add(self.ns1_name, self.ns1_records)
self.add(self.ns2_name, self.ns2_records)
@ -222,6 +227,7 @@ class DomainTestMixin(object):
self.validate_delete(self.MASTER_SERVER_ADDR, self.django_domain_name)
self.validate_delete(self.SLAVE_SERVER_ADDR, self.django_domain_name)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_bad_creation(self):
self.assertRaises((self.rest.ResponseStatusError, AssertionError),
self.add, self.domain_name, self.domain_records)

View File

@ -1,14 +1,12 @@
from django.contrib import admin
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 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 orchestra.admin.utils import admin_date, admin_link
from orchestra.admin.utils import admin_link, admin_date
class LogEntryAdmin(admin.ModelAdmin):
@ -36,12 +34,11 @@ class LogEntryAdmin(admin.ModelAdmin):
user_link = admin_link('user')
display_action_time = admin_date('action_time', short_description=_("Time"))
@mark_safe
def display_message(self, log):
edit = format_html('<a href="{url}"><img src="{img}"></img></a>', **{
edit = '<a href="%(url)s"><img src="%(img)s"></img></a>' % {
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
'img': static('admin/img/icon-changelink.svg'),
})
}
if log.is_addition():
return _('Added "%(link)s". %(edit)s') % {
'link': self.content_object_link(log),
@ -60,6 +57,7 @@ class LogEntryAdmin(admin.ModelAdmin):
}
display_message.short_description = _("Message")
display_message.admin_order_field = 'action_flag'
display_message.allow_tags = True
def display_action(self, log):
if log.is_addition():
@ -77,9 +75,10 @@ class LogEntryAdmin(admin.ModelAdmin):
url = reverse(view, args=(log.object_id,))
except NoReverseMatch:
return log.object_repr
return format_html('<a href="{}">{}</a>', url, log.object_repr)
return '<a href="%s">%s</a>' % (url, log.object_repr)
content_object_link.short_description = _("Content object")
content_object_link.admin_order_field = 'object_repr'
content_object_link.allow_tags = True
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
""" Add rel_opts and object to context """

View File

@ -1,12 +1,11 @@
from django import forms
from django.conf.urls import url
from django.contrib import admin
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db import models
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.html import format_html, strip_tags
from django.utils.safestring import mark_safe
from django.utils.html import strip_tags
from django.utils.translation import ugettext_lazy as _
from markdown import markdown
@ -51,7 +50,6 @@ class MessageReadOnlyInline(admin.TabularInline):
'all': ('orchestra/css/hide-inline-id.css',)
}
@mark_safe
def content_html(self, msg):
context = {
'number': msg.number,
@ -60,13 +58,12 @@ class MessageReadOnlyInline(admin.TabularInline):
}
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
header = '<strong style="color:#666;">%s</strong><hr />' % summary
content = markdown(msg.content)
content = content.replace('>\n', '>')
content = '<div style="padding-left:20px;">%s</div>' % content
return header + content
content_html.short_description = _("Content")
content_html.allow_tags = True
def has_add_permission(self, request):
return False
@ -114,10 +111,10 @@ class TicketInline(admin.TabularInline):
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
@mark_safe
def ticket_id(self, instance):
return '<b>%s</b>' % admin_link()(instance)
ticket_id.short_description = '#'
ticket_id.allow_tags = True
class TicketAdmin(ExtendedModelAdmin):
@ -138,7 +135,7 @@ class TicketAdmin(ExtendedModelAdmin):
'owner__username'
)
actions = (
mark_as_unread, mark_as_read, reject_tickets,
mark_as_unread, mark_as_read, 'delete_selected', reject_tickets,
resolve_tickets, close_tickets, take_tickets
)
sudo_actions = ('delete_selected',)
@ -195,7 +192,6 @@ class TicketAdmin(ExtendedModelAdmin):
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
@mark_safe
def display_summary(self, ticket):
context = {
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
@ -211,12 +207,14 @@ class TicketAdmin(ExtendedModelAdmin):
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
return '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % context
display_summary.short_description = 'Summary'
display_summary.allow_tags = True
def unbold_id(self, ticket):
""" Unbold id if ticket is read """
if ticket.is_read_by(self.user):
return format_html('<span style="font-weight:normal;font-size:11px;">{}</span>', ticket.pk)
return '<span style="font-weight:normal;font-size:11px;">%s</span>' % ticket.pk
return ticket.pk
unbold_id.allow_tags = True
unbold_id.short_description = "#"
unbold_id.admin_order_field = 'id'
@ -224,7 +222,8 @@ class TicketAdmin(ExtendedModelAdmin):
""" Bold subject when tickets are unread for request.user """
if ticket.is_read_by(self.user):
return ticket.subject
return format_html("<strong class='unread'>{}</strong>", ticket.subject)
return "<strong class='unread'>%s</strong>" % ticket.subject
bold_subject.allow_tags = True
bold_subject.short_description = _("Subject")
bold_subject.admin_order_field = 'subject'
@ -298,9 +297,10 @@ class QueueAdmin(admin.ModelAdmin):
num = queue.tickets__count
url = reverse('admin:issues_ticket_changelist')
url += '?queue=%i' % queue.pk
return format_html('<a href="{}">{}</a>', url, num)
return '<a href="%s">%d</a>' % (url, num)
num_tickets.short_description = _("Tickets")
num_tickets.admin_order_field = 'tickets__count'
num_tickets.allow_tags = True
def get_list_display(self, request):
""" show notifications """

View File

@ -1,5 +1,5 @@
from rest_framework import viewsets, mixins
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from orchestra.api import router, LogApiMixin
@ -13,13 +13,13 @@ class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
@action(detail=True)
@detail_route()
def mark_as_read(self, request, pk=None):
ticket = self.get_object()
ticket.mark_as_read_by(request.user)
return Response({'status': 'Ticket marked as read'})
@action(detail=True)
@detail_route()
def mark_as_unread(self, request, pk=None):
ticket = self.get_object()
ticket.mark_as_unread_by(request.user)

View File

@ -22,7 +22,7 @@ class MarkDownWidget(forms.Textarea):
)
markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text
def render(self, name, value, attrs, renderer=None):
def render(self, name, value, attrs):
widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name
textarea = super(MarkDownWidget, self).render(name, value, attrs)
preview = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import models, migrations
import orchestra.models.fields
from django.conf import settings
@ -21,7 +20,7 @@ class Migration(migrations.Migration):
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
('content', models.TextField(verbose_name='content')),
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
('author', models.ForeignKey(related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
],
options={
'get_latest_by': 'id',
@ -49,9 +48,9 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
('queue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, blank=True, related_name='tickets', null=True, to='issues.Queue')),
('creator', models.ForeignKey(related_name='tickets_created', null=True, to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('owner', models.ForeignKey(blank=True, related_name='tickets_owned', null=True, to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
('queue', models.ForeignKey(blank=True, related_name='tickets', null=True, to='issues.Queue')),
],
options={
'ordering': ['-updated_at'],
@ -61,14 +60,14 @@ class Migration(migrations.Migration):
name='TicketTracker',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
('ticket', models.ForeignKey(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')),
('ticket', models.ForeignKey(related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
('user', models.ForeignKey(related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
),
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'),
field=models.ForeignKey(related_name='messages', to='issues.Ticket', verbose_name='ticket'),
),
migrations.AlterUniqueTogether(
name='tickettracker',

View File

@ -1,114 +0,0 @@
# -*- 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'),
),
]

View File

@ -161,10 +161,10 @@ class Ticket(models.Model):
class Message(models.Model):
ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
verbose_name=_("ticket"), related_name='messages')
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
verbose_name=_("author"), related_name='ticket_messages')
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
related_name='messages')
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"),
related_name='ticket_messages')
author_name = models.CharField(_("author name"), max_length=256, blank=True)
content = models.TextField(_("content"))
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
@ -191,10 +191,9 @@ class Message(models.Model):
class TicketTracker(models.Model):
""" Keeps track of user read tickets """
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
verbose_name=_("ticket"), related_name='trackers')
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
verbose_name=_("user"), related_name='ticket_trackers')
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers')
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"),
related_name='ticket_trackers')
class Meta:
unique_together = (

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import django.db.models.deletion
import orchestra.core.validators
@ -23,8 +22,8 @@ class Migration(migrations.Migration):
('address_name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name', blank=True)),
('admin_email', models.EmailField(max_length=254, verbose_name='admin email', help_text='Administration email address')),
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('address_domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
('account', models.ForeignKey(related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('address_domain', models.ForeignKey(null=True, blank=True, to='domains.Domain', verbose_name='address domain')),
],
),
migrations.AlterUniqueTogether(

View File

@ -1,69 +0,0 @@
# -*- 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 &lt;name&gt;@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 &lt;name&gt;@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 &lt;name&gt;@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 &lt;name&gt;@lists.orchestra.lan', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
),
]

View File

@ -1,21 +0,0 @@
# -*- 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 &lt;name&gt;@lists.orchestra.lan', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
),
]

View File

@ -30,7 +30,7 @@ class List(models.Model):
admin_email = models.EmailField(_("admin email"),
help_text=_("Administration email address"))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='lists', on_delete=models.CASCADE)
related_name='lists')
# TODO also admin
is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this account should be treated as active. "

View File

@ -12,7 +12,7 @@ from .models import List
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta:
model = List.address_domain.field.related_model
model = List.address_domain.field.rel.to
fields = ('url', 'id', 'name')

View File

@ -1,26 +1,25 @@
import os
import smtplib
import time
import unittest
import requests
from unittest import skip
from email.mime.text import MIMEText
import requests
from django.conf import settings as djsettings
from django.core.management.base import CommandError
from django.urls import reverse
from django.core.urlresolvers import reverse
from selenium.webdriver.support.select import Select
from orchestra.admin.utils import change_url
from orchestra.contrib.domains.models import Domain
from orchestra.contrib.orchestration.models import Route, Server
from orchestra.contrib.orchestration.models import Server, Route
from orchestra.utils.sys import sshrun
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
save_response_on_error, snapshot_on_error)
from selenium.webdriver.support.select import Select
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, snapshot_on_error,
save_response_on_error)
from ... import backends, settings
from ...models import List
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
class ListMixin(object):
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
@ -32,8 +31,11 @@ class ListMixin(object):
def setUp(self):
super(ListMixin, self).setUp()
self.add_route()
djsettings.DEBUG = True
djsettings.CELERY_ALWAYS_EAGER = True
djsettings.CELERY_TASK_ALWAYS_EAGER = True
# import pdb; pdb.set_trace()
self.add_route()
def validate_add(self, name, address=None):
sshrun(self.MASTER_SERVER, 'list_members %s' % name, display=False)
@ -84,6 +86,7 @@ class ListMixin(object):
backend = backends.MailmanController.get_name()
Route.objects.create(backend=backend, match=True, host=server)
# @skip("Skip because not exists get_auth_token in orm.api.Api")
def test_add(self):
name = '%s_list' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -93,6 +96,7 @@ class ListMixin(object):
self.validate_login(name, password)
self.addCleanup(self.delete, name)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_add_with_address(self):
name = '%s_list' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -105,6 +109,7 @@ class ListMixin(object):
# Mailman doesn't support changing the address, only the domain
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_change_password(self):
name = '%s_list' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -116,6 +121,7 @@ class ListMixin(object):
self.change_password(name, new_password)
self.validate_login(name, new_password)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_change_domain(self):
name = '%s_list' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -131,6 +137,7 @@ class ListMixin(object):
self.update_domain(name, domain_name)
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_change_address_name(self):
name = '%s_list' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -145,6 +152,7 @@ class ListMixin(object):
self.update_address_name(name, address_name)
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_delete(self):
name = '%s_list' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -152,7 +160,8 @@ class ListMixin(object):
address_name = '%s_name' % random_ascii(10)
domain_name = '%sdomain.lan' % random_ascii(10)
address_domain = Domain.objects.create(name=domain_name, account=self.account)
self.add(name, password, admin_email, address_name=address_name, address_domain=address_domain)
self.add(name, password, admin_email, address_name=address_name,
address_domain=address_domain)
# Mailman doesn't support changing the address, only the domain
self.validate_add(name, address="%s@%s" % (address_name, address_domain))
self.delete(name)
@ -160,7 +169,6 @@ class ListMixin(object):
self.validate_delete(name)
@unittest.skipUnless(TEST_REST_API, "REST API tests")
class RESTListMixin(ListMixin):
def setUp(self):
super(RESTListMixin, self).setUp()
@ -228,6 +236,8 @@ class AdminListMixin(ListMixin):
domain_select.select_by_value(str(domain.pk))
name_field.submit()
# import pdb; pdb.set_trace()
# oop = Server.objects.all()
self.assertNotEqual(url, self.selenium.current_url)
@snapshot_on_error

View File

@ -3,10 +3,9 @@ from urllib.parse import parse_qs
from django import forms
from django.contrib import admin, messages
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db.models import F, Count, Value as V
from django.db.models.functions import Concat
from django.utils.html import format_html, format_html_join
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@ -83,7 +82,6 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
if settings.MAILBOXES_LOCAL_DOMAIN:
type(self).actions = self.actions + (SendMailboxEmail(),)
@mark_safe
def display_addresses(self, mailbox):
# Get from forwards
cache = caches.get_request_cache()
@ -95,7 +93,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
qs = qs.values_list('id', 'email', 'forward')
for addr_id, email, mbox in qs:
url = reverse('admin:mailboxes_address_change', args=(addr_id,))
link = format_html('<a href="{}">{}</a>', url, email)
link = '<a href="%s">%s</a>' % (url, email)
try:
cached_forwards[mbox].append(link)
except KeyError:
@ -109,23 +107,26 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
addresses = []
for addr in mailbox.addresses.all():
url = change_url(addr)
addresses.append(format_html('<a href="{}">{}</a>', url, addr.email))
addresses.append('<a href="%s">%s</a>' % (url, addr.email))
return '<br>'.join(addresses+forwards)
display_addresses.short_description = _("Addresses")
display_addresses.allow_tags = True
def display_forwards(self, mailbox):
forwards = mailbox.get_forwards()
return format_html_join(
'<br>', '<a href="{}">{}</a>',
[(change_url(addr), addr.email) for addr in forwards]
)
forwards = []
for addr in mailbox.get_forwards():
url = change_url(addr)
forwards.append('<a href="%s">%s</a>' % (url, addr.email))
return '<br>'.join(forwards)
display_forwards.short_description = _("Forward from")
display_forwards.allow_tags = True
@mark_safe
def display_filtering(self, mailbox):
""" becacuse of allow_tags = True """
return mailbox.get_filtering_display()
display_filtering.short_description = _("Filtering")
display_filtering.admin_order_field = 'filtering'
display_filtering.allow_tags = True
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'filtering':
@ -216,7 +217,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
elif obj.custom_filtering:
messages.warning(request, msg)
super(MailboxAdmin, self).save_model(request, obj, form, change)
obj.addresses.set(form.cleaned_data['addresses'])
obj.addresses = form.cleaned_data['addresses']
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
@ -246,27 +247,29 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def email_link(self, address):
link = self.domain_link(address)
return format_html("{}@{}", address.name, link)
return "%s@%s" % (address.name, link)
email_link.short_description = _("Email")
email_link.allow_tags = True
def display_mailboxes(self, address):
boxes = address.mailboxes.all()
return format_html_join(
mark_safe('<br>'), '<a href="{}">{}</a>',
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
)
boxes = []
for mailbox in address.mailboxes.all():
url = change_url(mailbox)
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
return '<br>'.join(boxes)
display_mailboxes.short_description = _("Mailboxes")
display_mailboxes.allow_tags = True
display_mailboxes.admin_order_field = 'mailboxes__count'
def display_all_mailboxes(self, address):
boxes = address.get_mailboxes()
return format_html_join(
mark_safe('<br>'), '<a href="{}">{}</a>',
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
)
boxes = []
for mailbox in address.get_mailboxes():
url = change_url(mailbox)
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
return '<br>'.join(boxes)
display_all_mailboxes.short_description = _("Mailboxes links")
display_all_mailboxes.allow_tags = True
@mark_safe
def display_forward(self, address):
forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()}
values = []
@ -278,6 +281,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
values.append(forward)
return '<br>'.join(values)
display_forward.short_description = _("Forward")
display_forward.allow_tags = True
display_forward.admin_order_field = 'forward'
def formfield_for_dbfield(self, db_field, **kwargs):

View File

@ -4,7 +4,7 @@ from orchestra.api import router, SetPasswordApiMixin, LogApiMixin
from orchestra.contrib.accounts.api import AccountApiMixin
from .models import Address, Mailbox
from .serializers import AddressSerializer, MailboxSerializer, MailboxWritableSerializer
from .serializers import AddressSerializer, MailboxSerializer
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
@ -17,12 +17,6 @@ class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets
queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
serializer_class = MailboxSerializer
def get_serializer_class(self):
if self.request.method == 'GET':
return self.serializer_class
return MailboxWritableSerializer
router.register(r'mailboxes', MailboxViewSet)
router.register(r'addresses', AddressViewSet)

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import django.db.models.deletion
import orchestra.contrib.mailboxes.validators
import django.core.validators
@ -22,8 +21,8 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(verbose_name='name', validators=[orchestra.contrib.mailboxes.validators.validate_emailname], blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64)),
('forward', models.CharField(verbose_name='forward', validators=[orchestra.contrib.mailboxes.validators.validate_forward], blank=True, help_text='Space separated email addresses or mailboxes', max_length=256)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Domain', related_name='addresses', verbose_name='domain')),
('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='addresses', verbose_name='Account')),
('domain', models.ForeignKey(to='domains.Domain', related_name='addresses', verbose_name='domain')),
],
options={
'verbose_name_plural': 'addresses',
@ -36,7 +35,7 @@ class Migration(migrations.Migration):
('subject', models.CharField(verbose_name='subject', max_length=256)),
('message', models.TextField(verbose_name='message')),
('enabled', models.BooleanField(verbose_name='enabled', default=False)),
('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='mailboxes.Address', related_name='autoresponse', verbose_name='address')),
('address', models.OneToOneField(to='mailboxes.Address', related_name='autoresponse', verbose_name='address')),
],
),
migrations.CreateModel(
@ -48,7 +47,7 @@ class Migration(migrations.Migration):
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('REDIRECT', 'Archive spam (X-Spam-Score&ge;9)'), ('DISABLE', 'Disable'), ('REJECT', 'Reject spam (X-Spam-Score&ge;9)')], max_length=16, default='REDIRECT')),
('custom_filtering', models.TextField(verbose_name='filtering', validators=[orchestra.contrib.mailboxes.validators.validate_sieve], blank=True, help_text='Arbitrary email filtering in sieve language. This overrides any automatic junk email filtering')),
('is_active', models.BooleanField(verbose_name='active', default=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, related_name='mailboxes', verbose_name='account')),
('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='mailboxes', verbose_name='account')),
],
options={
'verbose_name_plural': 'mailboxes',

View File

@ -1,71 +0,0 @@
# -*- 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&ge;8)'), ('REDIRECT5', 'Archive spam (Score&ge;5)'), ('REJECT', 'Reject spam (Score&ge;8)'), ('REJECT5', 'Reject spam (Score&ge;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')]),
),
]

View File

@ -23,7 +23,7 @@ class Mailbox(models.Model):
])
password = models.CharField(_("password"), max_length=128)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='mailboxes', on_delete=models.CASCADE)
related_name='mailboxes')
filtering = models.CharField(max_length=16,
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())])
@ -44,7 +44,7 @@ class Mailbox(models.Model):
def active(self):
try:
return self.is_active and self.account.is_active
except type(self).account.field.related_model.DoesNotExist:
except type(self).account.field.rel.to.DoesNotExist:
return self.is_active
def disable(self):
@ -97,14 +97,14 @@ class Address(models.Model):
validators=[validators.validate_emailname],
help_text=_("Address name, left blank for a <i>catch-all</i> address"))
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
verbose_name=_("domain"), related_name='addresses', on_delete=models.CASCADE)
verbose_name=_("domain"), related_name='addresses')
mailboxes = models.ManyToManyField(Mailbox, verbose_name=_("mailboxes"),
related_name='addresses', blank=True)
forward = models.CharField(_("forward"), max_length=256, blank=True,
validators=[validators.validate_forward],
help_text=_("Space separated email addresses or mailboxes"))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='addresses', on_delete=models.CASCADE)
related_name='addresses')
class Meta:
verbose_name_plural = _("addresses")
@ -168,7 +168,7 @@ class Address(models.Model):
class Autoresponse(models.Model):
address = models.OneToOneField(Address, verbose_name=_("address"),
related_name='autoresponse', on_delete=models.CASCADE)
related_name='autoresponse')
# TODO initial_date
subject = models.CharField(_("subject"), max_length=256)
message = models.TextField(_("message"))

View File

@ -1,4 +1,3 @@
from django.db import transaction
from rest_framework import serializers
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer
@ -9,7 +8,7 @@ from .models import Mailbox, Address
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta:
model = Address.domain.field.related_model
model = Address.domain.field.rel.to
fields = ('url', 'id', 'name')
@ -36,41 +35,6 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer
postonly_fields = ('name', 'password')
class AddressRelatedField(serializers.HyperlinkedRelatedField):
# Filter addresses by account (user)
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(account=self.context['account'])
class MailboxWritableSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
addresses = AddressRelatedField(many=True, view_name='address-detail', queryset=Address.objects.all())
class Meta:
model = Mailbox
fields = (
'url', 'id', 'name', 'password', 'filtering', 'custom_filtering', 'addresses', 'is_active'
)
postonly_fields = ('name', 'password')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['addresses'].context['account'] = self.account
@transaction.atomic
def create(self, validated_data):
addresses = validated_data.pop('addresses', [])
instance = super().create(validated_data)
instance.addresses.set(addresses)
return instance
@transaction.atomic
def update(self, instance, validated_data):
addresses = validated_data.pop('addresses', [])
instance.addresses.set(addresses)
return super().update(instance, validated_data)
class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta:
model = Mailbox
@ -79,7 +43,7 @@ class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSe
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
domain = RelatedDomainSerializer()
mailboxes = RelatedMailboxSerializer(many=True, required=False)
mailboxes = RelatedMailboxSerializer(many=True, required=False) #allow_add_remove=True
class Meta:
model = Address
@ -87,21 +51,6 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri
def validate(self, attrs):
attrs = super(AddressSerializer, self).validate(attrs)
mailboxes = attrs.get('mailboxes', [])
forward = attrs.get('forward', '')
if not mailboxes and not forward:
if not attrs['mailboxes'] and not attrs['forward']:
raise serializers.ValidationError("A mailbox or forward address should be provided.")
return attrs
@transaction.atomic
def create(self, validated_data):
mailboxes = validated_data.pop('mailboxes', [])
obj = super().create(validated_data)
obj.mailboxes.set(mailboxes)
return obj
@transaction.atomic
def update(self, instance, validated_data):
mailboxes = validated_data.pop('mailboxes', [])
instance.mailboxes.set(mailboxes)
return super().update(instance, validated_data)

View File

@ -27,7 +27,7 @@ def create_local_address(sender, *args, **kwargs):
mbox = kwargs['instance']
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
if not mbox.pk and local_domain:
Domain = Address._meta.get_field('domain').remote_field.model
Domain = Address._meta.get_field('domain').rel.to
try:
domain = Domain.objects.get(name=local_domain)
except Domain.DoesNotExist:

View File

@ -4,14 +4,14 @@ import poplib
import smtplib
import time
import textwrap
import unittest
from email.mime.text import MIMEText
from unittest import skip
from django.apps import apps
from django.conf import settings as djsettings
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import CommandError
from django.urls import reverse
from django.core.urlresolvers import reverse
from selenium.webdriver.support.select import Select
from orchestra.contrib.orchestration.models import Server, Route
@ -22,8 +22,6 @@ from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot
from ... import backends, settings
from ...models import Mailbox
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
class MailboxMixin(object):
MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost')
@ -51,7 +49,6 @@ class MailboxMixin(object):
Resource.objects.create(
name='disk',
content_type=ContentType.objects.get_for_model(Mailbox),
period=Resource.LAST,
verbose_name='Mail quota',
unit='MB',
scale=10**6,
@ -111,6 +108,7 @@ class MailboxMixin(object):
home = Mailbox.objects.get(name=username).get_home()
sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_add(self):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -119,6 +117,7 @@ class MailboxMixin(object):
imap = self.login_imap(username, password)
self.validate_mailbox(username)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_change_password(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -129,6 +128,7 @@ class MailboxMixin(object):
self.change_password(username, new_password)
imap = self.login_imap(username, new_password)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_quota(self):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -143,6 +143,7 @@ class MailboxMixin(object):
imap_quota = int(imap.getquotaroot("INBOX")[1][1][0].split(' ')[-1].split(')')[0])
self.assertEqual(quota*1024, imap_quota)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_send_email(self):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -159,6 +160,7 @@ class MailboxMixin(object):
finally:
server.quit()
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_address(self):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -172,6 +174,7 @@ class MailboxMixin(object):
self.send_email("%s@%s" % (name, domain), token)
self.validate_email(username, token)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_disable(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -182,6 +185,7 @@ class MailboxMixin(object):
self.disable(username)
self.assertRaises(imap.error, self.login_imap, username, password)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_delete(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%sppppP001' % random_ascii(5)
@ -197,6 +201,7 @@ class MailboxMixin(object):
self.assertRaises(CommandError,
sshrun, self.MASTER_SERVER, 'ls %s' % home, display=False)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_delete_address(self):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -213,6 +218,7 @@ class MailboxMixin(object):
self.send_email("%s@%s" % (name, domain), token)
self.validate_email(username, token)
@skip("Skip because not exists get_auth_token in orm.api.Api")
def test_custom_filtering(self):
username = '%s_mailbox' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
@ -238,7 +244,6 @@ class MailboxMixin(object):
# TODO test autoreply
@unittest.skipUnless(TEST_REST_API, "REST API tests")
class RESTMailboxMixin(MailboxMixin):
def setUp(self):
super(RESTMailboxMixin, self).setUp()

View File

@ -1,4 +1,4 @@
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.shortcuts import redirect

View File

@ -3,11 +3,9 @@ import email
from django import forms
from django.contrib import admin
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db.models import Count
from django.shortcuts import redirect
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
@ -62,10 +60,11 @@ class MessageAdmin(ExtendedModelAdmin):
def display_subject(self, instance):
subject = instance.subject
if len(subject) > 64:
return mark_safe(subject[:64] + '&hellip;')
return subject[:64] + '&hellip;'
return subject
display_subject.short_description = _("Subject")
display_subject.admin_order_field = 'subject'
display_subject.allow_tags = True
def display_retries(self, instance):
num_logs = instance.logs__count
@ -75,9 +74,10 @@ class MessageAdmin(ExtendedModelAdmin):
else:
url = reverse('admin:mailer_smtplog_changelist')
url += '?&message=%i' % instance.pk
return format_html('<a href="{}" onclick="return showAddAnotherPopup(this);">{}</a>', url, instance.retries)
return '<a href="%s" onclick="return showAddAnotherPopup(this);">%d</a>' % (url, instance.retries)
display_retries.short_description = _("Retries")
display_retries.admin_order_field = 'retries'
display_retries.allow_tags = True
def display_content(self, instance):
part = email.message_from_string(instance.content)
@ -99,8 +99,9 @@ class MessageAdmin(ExtendedModelAdmin):
payload = payload.decode(charset)
if part.get_content_type() == 'text/plain':
payload = payload.replace('\n', '<br>').replace(' ', '&nbsp;')
return mark_safe(payload)
return payload
display_content.short_description = _("Content")
display_content.allow_tags = True
def display_full_subject(self, instance):
return instance.subject

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import models, migrations
@ -33,7 +32,7 @@ class Migration(migrations.Migration):
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
('date', models.DateTimeField(auto_now_add=True)),
('log_message', models.TextField()),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailer.Message', editable=False, related_name='logs')),
('message', models.ForeignKey(to='mailer.Message', editable=False, related_name='logs')),
],
),
]

View File

@ -1,89 +0,0 @@
# -*- 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'),
),
]

View File

@ -67,7 +67,7 @@ class SMTPLog(models.Model):
(SUCCESS, _("Success")),
(FAILURE, _("Failure")),
)
message = models.ForeignKey(Message, editable=False, related_name='logs', on_delete=models.CASCADE)
message = models.ForeignKey(Message, editable=False, related_name='logs')
result = models.CharField(max_length=16, choices=RESULTS, default=SUCCESS)
date = models.DateTimeField(auto_now_add=True)
log_message = models.TextField()

View File

@ -1,8 +1,7 @@
from django import forms
from django.contrib import admin
from django.urls import reverse
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@ -39,13 +38,15 @@ class MiscServiceAdmin(ExtendedModelAdmin):
actions = (disable, enable)
def display_name(self, misc):
return format_html('<span title="{}">{}</span>', misc.description, misc.name)
return '<span title="%s">%s</span>' % (misc.description, misc.name)
display_name.short_description = _("name")
display_name.allow_tags = True
display_name.admin_order_field = 'name'
def display_verbose_name(self, misc):
return format_html('<span title="{}">{}</span>', misc.description, misc.verbose_name)
return '<span title="%s">%s</span>' % (misc.description, misc.verbose_name)
display_verbose_name.short_description = _("verbose name")
display_verbose_name.allow_tags = True
display_verbose_name.admin_order_field = 'verbose_name'
def num_instances(self, misc):

View File

@ -2,7 +2,6 @@
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
import orchestra.core.validators
from django.conf import settings
import orchestra.models.fields
@ -23,7 +22,7 @@ class Migration(migrations.Migration):
('description', models.TextField(blank=True, verbose_name='description')),
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='miscellaneous', verbose_name='account', to=settings.AUTH_USER_MODEL)),
('account', models.ForeignKey(related_name='miscellaneous', verbose_name='account', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'miscellaneous',
@ -44,6 +43,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='miscellaneous',
name='service',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', verbose_name='service', to='miscellaneous.MiscService'),
field=models.ForeignKey(related_name='instances', verbose_name='service', to='miscellaneous.MiscService'),
),
]

View File

@ -1,59 +0,0 @@
# -*- 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'),
),
]

View File

@ -42,10 +42,10 @@ class MiscService(models.Model):
class Miscellaneous(models.Model):
service = models.ForeignKey(MiscService, on_delete=models.CASCADE,
verbose_name=_("service"), related_name='instances')
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("account"), related_name='miscellaneous')
service = models.ForeignKey(MiscService, verbose_name=_("service"),
related_name='instances')
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='miscellaneous')
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
db_index=True, help_text=_("A unique identifier for this service."))
description = models.TextField(_("description"), blank=True)

View File

@ -39,10 +39,10 @@ class Operation():
self.routes = routes
@classmethod
def execute(cls, operations, serialize=False, run_async=None):
def execute(cls, operations, serialize=False, async=None):
from . import manager
scripts, backend_serialize = manager.generate(operations)
return manager.execute(scripts, serialize=(serialize or backend_serialize), run_async=run_async)
return manager.execute(scripts, serialize=(serialize or backend_serialize), async=async)
@classmethod
def create_for_action(cls, instances, action):

View File

@ -30,14 +30,14 @@ STATE_COLORS = {
class RouteAdmin(ExtendedModelAdmin):
list_display = (
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'run_async',
'display_backend', 'host', 'match', 'display_model', 'display_actions', 'async',
'is_active'
)
list_editable = ('host', 'match', 'run_async', 'is_active')
list_filter = ('host', 'is_active', 'run_async', 'backend')
list_editable = ('host', 'match', 'async', 'is_active')
list_filter = ('host', 'is_active', 'async', 'backend')
list_prefetch_related = ('host',)
ordering = ('backend',)
add_fields = ('backend', 'host', 'match', 'run_async', 'is_active')
add_fields = ('backend', 'host', 'match', 'async', 'is_active')
change_form = RouteForm
actions = (orchestrate,)
change_view_actions = actions
@ -51,18 +51,19 @@ class RouteAdmin(ExtendedModelAdmin):
def display_model(self, route):
try:
return route.backend_class.model
return escape(route.backend_class.model)
except KeyError:
return mark_safe("<span style='color: red;'>NOT AVAILABLE</span>")
return "<span style='color: red;'>NOT AVAILABLE</span>"
display_model.short_description = _("model")
display_model.allow_tags = True
@mark_safe
def display_actions(self, route):
try:
return '<br>'.join(route.backend_class.get_actions())
except KeyError:
return "<span style='color: red;'>NOT AVAILABLE</span>"
display_actions.short_description = _("actions")
display_actions.allow_tags = True
def formfield_for_dbfield(self, db_field, **kwargs):
""" Provides dynamic help text on backend form field """
@ -119,6 +120,7 @@ class BackendOperationInline(admin.TabularInline):
return _("Deleted {0}").format(operation.instance_repr or '-'.join(
(escape(operation.content_type), escape(operation.object_id))))
return link
instance_link.allow_tags = True
instance_link.short_description = _("Instance")
def has_add_permission(self, *args, **kwargs):
@ -177,12 +179,14 @@ class ServerAdmin(ExtendedModelAdmin):
change_view_actions = actions
def display_ping(self, instance):
return mark_safe(self._remote_state[instance.pk][0])
return self._remote_state[instance.pk][0]
display_ping.short_description = _("Ping")
display_ping.allow_tags = True
def display_uptime(self, instance):
return mark_safe(self._remote_state[instance.pk][1])
return self._remote_state[instance.pk][1]
display_uptime.short_description = _("Uptime")
display_uptime.allow_tags = True
def get_queryset(self, request):
""" Order by structured name and imporve performance """

Some files were not shown because too many files have changed in this diff Show More