Compare commits

...

128 commits

Author SHA1 Message Date
Santiago L 5ab4779e1a Fix kombu dependencies on requirements.txt 2022-01-18 15:05:45 +01:00
Santiago L 5e6cd2f147
Merge pull request #12 from ribaguifi/dev/api-writable
Update some API endpoints to make it writable
2021-11-24 11:03:17 +01:00
Santiago L 03666d8ed0 Filter related addresses by account 2021-10-14 13:03:08 +02:00
Santiago L e88e27a56e Make MailboxViewSet writable: create & update 2021-10-07 14:14:21 +02:00
Santiago L 9a4f4ee17c Fix SetPasswordHyperlinkedSerializer (update to new DRF) 2021-10-07 14:11:50 +02:00
Santiago L 008f49100f Fix display_mailboxes format (mark HTML as safe) 2021-09-24 13:54:34 +02:00
Santiago L b0f77ad591 Merge branch 'master' into dev/api-writable 2021-07-13 13:25:00 +02:00
Santiago L 639ecdde58
Merge pull request #9 from ribaguifi/dev/django2.2
Upgrade to Django 2.2.X series
2021-07-13 13:20:30 +02:00
Santiago L 361b4b41a8 Upgrade Pygments and phonenumbers libraries 2021-07-13 12:55:35 +02:00
Santiago L 6720df314b Bump jsonfield version to 3.1.0 2021-07-13 12:46:20 +02:00
Santiago L 9f80c75da7 Drop unused python-ecdsa dependency 2021-07-13 12:09:29 +02:00
Santiago L 1258a27688 Bump django-localflavor version to 3.1
See related issue #10
2021-07-13 12:01:27 +02:00
Santiago L a400c25de9 Upgrade django-filter & django-extensions 2021-07-13 11:48:41 +02:00
Santiago L e3ec82a182 Bump django-rest-framework version to 3.12.4 2021-07-13 11:24:07 +02:00
Santiago L cda47e2fb6 Remove deprecated passlib setting __vary_rounds
Deprecated since version 1.7 and will be removed in 2.0
The (very minimal) security benefit it provides was judged to not
be worth code complexity it requires.
2021-07-09 13:03:53 +02:00
Santiago L d3e5ea59a9 Bump passlib version to 1.7.4 2021-07-09 12:34:43 +02:00
Santiago L b37d9cc515 Remove unneeded custom 'delete_selected'
`has_delete_permission` already avoids deleting main system users
2021-07-08 14:48:51 +02:00
Santiago L 1faab905d6 Remove duplicated 'delete_selected' action of TicketAdmin
Fixes admin.E130 error
2021-07-08 13:58:37 +02:00
Santiago L de26baf75a Refactor TransactionProcessAdmin.delete_selected override
Override `delete_queryset` instead of overriding `delete_selected`
action. Fixes admin.E130 error.
Related ticket https://github.com/django/django/pull/10603
2021-07-08 13:49:24 +02:00
Santiago L 50f916fa4d Replace base_name => basename for consisntency's sake
Changed on DRF 3.12.x and related to PR https://github.com/encode/django-rest-framework/pull/5990
2021-07-08 12:46:55 +02:00
Santiago L c21a52a756 Bump django-rest-framework version to 3.12.2 2021-07-08 12:46:17 +02:00
Santiago L a90e500186 Bump Django version to 2.2.24 2021-07-08 12:32:53 +02:00
Santiago L 7d6a2474ab Handle missing url attribute on write requests 2021-07-08 12:25:29 +02:00
Santiago L b365580165 Merge branch 'master' into dev/api-writable 2021-06-22 14:13:28 +02:00
Santiago L bcfed9cb79 Use BaseCommand on orchestraversion
class NoArgsCommand has been removed on Django 1.10
2021-06-22 14:11:06 +02:00
Santiago L 867d9afe65 Make /aoi/addresses/ endpoint writable 2021-06-18 11:11:50 +02:00
Santiago L e1d71fa620 Add support to create Address via API 2021-06-08 13:37:00 +02:00
Santiago L 70f7551e7d Replace Router.get_default_base_name by Router.get_default_basename
Deprecated in DRF 3.9.0
2021-06-08 13:34:36 +02:00
Santiago L 81c67778e5 Fix RelatedDomainSerializer model
Regression introduced by 7d975637d5
partially fixed on 48ef1f21e3
2021-06-08 12:58:36 +02:00
Santiago L 9a3b6dcbc3 Add 'exclude' attribute to TransactionSerializer
Creating a ModelSerializer without either the 'fields' attribute or the
'exclude' attribute has been deprecated since 3.3.0
2021-06-08 10:23:04 +02:00
Santiago L 5e7a823205 Revert "documentation ribaguifi style instalation"
This reverts commit 5b4b7310e6.
Remove duplicated project settings template.
2021-06-08 10:05:40 +02:00
Santiago L e1224ddd57 Add django_filters to INSTALLED_APPS
Fix TemplateDoesNotExist django_filters/rest_framework/form.html
2021-06-08 10:02:59 +02:00
Santiago L 7b59931bcf
Merge pull request #8 from ribaguifi/dev/django2.1-admin
Refactor admin code to support Django 2.1
2021-05-24 12:55:35 +02:00
Santiago L 0e10d2b142 Bump python-dateutil to 2.7.0+ 2021-05-24 12:53:50 +02:00
Santiago L 47eb0f1efe Rename local var display because shadows built-in 2021-05-24 12:37:36 +02:00
Santiago L 28c03ac6c8 Handle HTML safe rendering on accounts, bills & payments
Drop `allow_tags` attribute which has been removed on Django 2.0
2021-05-24 12:36:49 +02:00
Santiago L 9953124a95 Replace Context by dict
Since Django 1.10 template objects returned by get_template() and
select_template() no longer accept a Context in their render() method.
2021-05-24 11:19:30 +02:00
Santiago L 06c226d302 Handle HTML safe rendering on webapps & miscellaneous
Drop `allow_tags` attribute which has been removed on Django 2.0
2021-05-21 11:17:06 +02:00
Santiago L 4f695c2e6e Handle HTML safe rendering on orchestration, resources & history
Drop `allow_tags` attribute which has been removed on Django 2.0
2021-05-21 10:47:27 +02:00
Santiago L e6495a967b Handle HTML safe rendering on issues, plans & saas
Drop `allow_tags` attribute which has been removed on Django 2.0
2021-05-21 10:07:59 +02:00
Santiago L 6d8a2ced53 Context shoud be dict on render_email_template()
template.Context intance is no longer accepted
2021-05-20 14:08:09 +02:00
Santiago L a2927f7616 Add required param renderer to MarkDownWidget 2021-05-20 14:02:10 +02:00
Santiago L f13fea5030 Fix display format on accounts, databases...
domains, mailboxes & mailer

Drop `allow_tags` attribute which has been removed on Django 2.0
2021-05-20 13:58:16 +02:00
Santiago L f0683660ae Fix display format on bills, orders & services
Drop `allow_tags` attribute which has been removed on Django 2.0
2021-05-17 14:15:12 +02:00
Santiago L b24ddf7546 Handle empty ping response 2021-05-17 13:22:08 +02:00
Santiago L 3b4bb51925 Fix display format on SaaS & Sever admin list
mark_safe generated HTML
2021-05-17 13:20:18 +02:00
Santiago L a6c5aa32df Fix Mailbox creation.
Direct assignment to the reverse side of a many-to-many set is
prohibited. Use addresses.set() instead.
2021-05-17 12:54:16 +02:00
Santiago L 13b4ac5eee Add required param renderer to ReadOnlyPasswordHashWidget 2021-05-13 14:42:05 +02:00
Santiago L 8dc792b851 Fix render() of PaddingCheckboxSelectMultiple widget 2021-05-13 12:37:17 +02:00
Santiago L 5a21f766b4 Add required param renderer to Widget.render
Added on Django 1.11 and required since 2.1
The renderer argument is added to the Widget.render() method.
https://docs.djangoproject.com/en/2.1/releases/1.11/#id2
2021-05-13 11:52:34 +02:00
Santiago L 7183174f4c Handle empty address on Server.clean() 2021-05-13 10:57:48 +02:00
Santiago L 48ef1f21e3 Navigate through FK field to related model
Fix regression introduced by 7d975637d5
when there is a misunderstanding while replacing deprecated rel.to
2021-05-12 14:38:17 +02:00
Santiago L aebbd424fc Fix admin list_display with HTML content 2021-05-12 14:16:28 +02:00
Santiago L 5389f425ce mark_safe display_websites & display_addresses 2021-05-12 13:55:47 +02:00
Santiago L ed9bfc0eb7 Merge branch 'dev/django2.1-middleware' 2021-05-11 14:20:51 +02:00
Santiago L 0095da61ea Bump to Markdown==3.3.4
Required by Django Rest Framework
2021-05-11 14:02:53 +02:00
Santiago L 58be94bde2 Upgrade orchestra middlewares
Refactor to changes introduced on Django 1.10
https://docs.djangoproject.com/en/2.1/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware
2021-05-11 14:00:41 +02:00
Santiago L be5e06129a Add password validation settings
New on Django 1.9
2021-05-11 13:48:26 +02:00
Santiago L 69df9780bf Update setting MIDDLEWARE_CLASSES to MIDDLEWARE 2021-05-11 13:47:33 +02:00
Santiago L 18a41d507b
Merge pull request #7 from ribaguifi/dev/django2.1
Upgrade to Django 2.1
2021-05-11 13:06:52 +02:00
Santiago L f7627926cb Replace detail_route with action decorator
DRF 3.10.0 deprecates the detail_route decorator in favor of action
2021-05-06 13:08:21 +02:00
Santiago L ffd08459c4 Bump to django-rest-framework 3.10.3 2021-05-06 13:07:56 +02:00
Santiago L 085b8f85bd Bump to django-filter 2.2.0
Compatible with Django 2.1
2021-05-06 12:58:54 +02:00
Santiago L d5fce3b6e2 Replace string_concat() with format_lazy()
On Django 1.11 django.utils.translation.string_concat() is
deprecated in favor of django.utils.text.format_lazy()
and it has been removed on Django 2.1
2021-05-06 12:07:17 +02:00
Santiago L 777a7f6de5 Bump to Django 2.1 2021-05-06 12:06:31 +02:00
Santiago L 422305a636
Merge pull request #6 from ribaguifi/dev/python36support
Refactor to support Python 3.6 and Django 2.0
2021-05-06 11:08:46 +02:00
Santiago L d6cebf66a2 DjangoFilterBackend moved to django_filters 2021-05-06 10:57:11 +02:00
Santiago L 0338b927cf query.order_by is a tuple, update value to compare 2021-04-22 14:51:03 +02:00
Santiago L 97f1c7ef2b Replace field.rel.to with field.remote_field.model
Field.rel and Field.remote_field.to are removed in Django 2.x
2021-04-22 14:44:47 +02:00
Santiago L b6cf0c34f5 Call User.is_authenticated and User.is_anonymous as properties 2021-04-22 14:31:05 +02:00
Santiago L 7fa7106d72 Update migrations to include mandatory on_delete
Django 2.0
2021-04-22 14:18:01 +02:00
Santiago L 6ef7f921e9 Upgrade requirements to Django 2.0
Update too related dependencies.
2021-04-22 13:40:54 +02:00
Santiago L a8b17da992 Squash migrations 2021-04-22 13:29:09 +02:00
Santiago L c689a6e44c Fix Message.author on_cascade because cannot be null. 2021-04-22 10:52:33 +02:00
Santiago L de979011f9 Deprecated Passing a 3-tuple or an app_name to include()
Django 1.9
2021-04-22 10:51:23 +02:00
Santiago L 7d975637d5 Replace ForeignKey.field.rel.to --> field.model
rel.to dropped on Django 2.0
2021-04-22 10:44:09 +02:00
Santiago L d863598d81 Define on_delete argument for ForeignKey and OneToOneField
Required since Django 2.0
2021-04-22 10:28:00 +02:00
Santiago L eadc06d4c5 django.core.urlresolvers moved to django.urls
Django 2.0
2021-04-21 14:27:18 +02:00
Santiago L 2b06652a5b Handle edge cases of last day of the month of billing period. 2021-03-31 12:11:53 +02:00
Santiago L dc722ec17a Set env variable to skip REST_API tests. 2021-03-31 10:39:47 +02:00
Santiago L e7aabf4799 Python3 requires to open explicitly on binary mode to write bytes 2021-03-30 17:46:42 +02:00
Cayo Puigdefabregas fa8a895299 fixing test traffic monitors is a list 2021-03-30 15:21:50 +02:00
Cayo Puigdefabregas 091120d3c2 fixing test_traffic get_total 2021-03-30 15:21:23 +02:00
Cayo Puigdefabregas c952d782cd fixing mailbox test 2021-03-30 15:21:00 +02:00
Cayo Puigdefabregas 226327cacf fixing job test 2021-03-30 15:20:37 +02:00
Cayo Puigdefabregas 6f043cd272 fixing ftp test 2021-03-30 15:20:25 +02:00
Cayo Puigdefabregas 0633df114e fixing DomainBillingTest 2021-03-30 15:19:39 +02:00
Cayo Puigdefabregas a53b71bab1 fixed choices 2021-03-30 15:19:19 +02:00
Cayo Puigdefabregas c010c10157 fixed admin_login in test 2021-03-30 14:28:15 +02:00
Santiago L acac7727c2 Fix services tests 2021-03-30 14:27:21 +02:00
Cayo Puigdefabregas 48ef6d63bc Fixed bug in python backend 2021-03-30 14:25:10 +02:00
Santiago L 45bf31c9da Fix freezegun version 2021-03-30 14:19:17 +02:00
Santiago L 08a76a8de4 Ignore Account.is_staff kwarg (auth.AbstractBaseUser) 2021-03-30 13:56:04 +02:00
Santiago L 14fbd98e33 Refactor PHP tests dropping legacy controller (backend) 2021-03-30 13:35:00 +02:00
Santiago L 58395147c9 Replace PasswdVirtualUserBackend with RoundcubeIdentityController 2021-03-30 13:13:26 +02:00
Santiago L c505f9a3c6 Replace SystemUserBackend with UNIXUserController 2021-03-30 13:11:41 +02:00
Santiago L f4c0a7413c Generate missing migrations. 2021-03-30 12:52:12 +02:00
Santiago L 9d2d0befc4 Rename async--> run_async
On Python3.5 async becames a reserved keyword.
2021-03-30 12:51:12 +02:00
cayop 350d93f820
Merge pull request #3 from ribaguifi/docker
Docker and deployment
2021-02-01 12:45:15 +01:00
Cayo Puigdefabregas cedb8d690b swap tabs for spaces 2021-02-01 12:37:45 +01:00
Cayo Puigdefabregas 883cf631e2 mv orchestra-deploy to deploy.sh 2021-01-30 15:17:03 +01:00
Cayo Puigdefabregas 898c6882c8 fixed stop and restart services 2021-01-30 15:09:30 +01:00
Cayo Puigdefabregas a236bbdf5d fixed start services 2021-01-30 15:06:48 +01:00
Cayo Puigdefabregas 6ce4d6b877 fixed make_options for all commands 2021-01-30 14:59:49 +01:00
Cayo Puigdefabregas 8da89ae22a fixing commands we need 2021-01-30 14:17:18 +01:00
Cayo Puigdefabregas 78db4fb8d5 fixed 2021-01-30 13:55:27 +01:00
Cayo Puigdefabregas 7c62092faa fixed deploy 2021-01-30 13:43:03 +01:00
Cayo Puigdefabregas d0050f81b7 clean orchestra-deploy 2021-01-30 13:18:03 +01:00
Cayo Puigdefabregas 6450d0d749 clean code for work in master 2021-01-29 14:11:54 +01:00
Cayo Puigdefabregas 2619a50410 change branch 2021-01-29 13:04:22 +01:00
Cayo Puigdefabregas 24e75bc07f fixed pg 2021-01-29 11:44:10 +01:00
Cayo Puigdefabregas 0cde41042f wkhtmltopdf 2021-01-28 20:07:17 +01:00
Cayo Puigdefabregas 5b4b7310e6 documentation ribaguifi style instalation 2021-01-28 19:58:48 +01:00
Cayo Puigdefabregas 38275847d9 fixing results 2021-01-28 19:57:42 +01:00
Cayo Puigdefabregas a4c3b00205 Fixed comand line of setuppostgress 2021-01-28 16:47:54 +01:00
Cayo Puigdefabregas c386b10bc8 Fixed 2021-01-28 16:47:26 +01:00
Cayo Puigdefabregas 0b937bfb4f up orchestra-deploy 2021-01-28 12:34:53 +01:00
Cayo Puigdefabregas 30bd1ad816 fixed path 2021-01-28 12:25:43 +01:00
Cayo Puigdefabregas 44ebd42942 adapt docker env for new version of debian 2021-01-28 12:20:45 +01:00
Cayo Puigdefabregas e2ef8823f8 modify source of orchestra-admin 2021-01-27 15:51:16 +01:00
Cayo Puigdefabregas f0fadf8bba modify Docker 2021-01-27 15:49:02 +01:00
Santiago L e6e434f525
Merge pull request #2 from ribaguifi/pangea-production-src
Pangea production source
2021-01-13 16:43:49 +01:00
Santiago L 43d8c9471b Latest Pangea source code deployed to production. 2021-01-13 16:41:36 +01:00
Marc Aymerich ea9c398de4 Disabled database hostname validator to allow for % host 2017-07-07 09:30:35 +00:00
Marc Aymerich 6fadf0c631 PHP support for sockets (vs TCP) improved 2017-07-05 17:52:37 +00:00
Marc Aymerich a1f73d883a Added support for multiple webservers 2017-07-05 13:55:13 +00:00
Marc Aymerich 0c1b4c7f4a Added nextcloud support 2017-06-09 08:28:44 +00:00
Marc Aymerich 25fbc6a088 switched container engine to lxc 2017-06-09 08:26:40 +00:00
290 changed files with 6967 additions and 2637 deletions

View file

@ -11,7 +11,7 @@ If you are planing to do some development you may want to consider doing it unde
2. Build a new image, create and start a container 2. Build a new image, create and start a container
```bash ```bash
curl -L http://git.io/orchestra-Dockerfile > /tmp/Dockerfile curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/Dockerfile > /tmp/Dockerfile
docker build -t orchestra /tmp/ docker build -t orchestra /tmp/
docker create --name orchestra -i -t -u orchestra -w /home/orchestra orchestra bash docker create --name orchestra -i -t -u orchestra -w /home/orchestra orchestra bash
docker start orchestra docker start orchestra
@ -21,12 +21,13 @@ If you are planing to do some development you may want to consider doing it unde
3. Deploy django-orchestra development environment, inside the container 3. Deploy django-orchestra development environment, inside the container
```bash ```bash
bash <( curl -L http://git.io/orchestra-deploy ) --dev bash <( curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/deploy.sh ) --dev
``` ```
3. Nginx should be serving on port 80, but Django's development server can be used as well: 3. Nginx should be serving on port 80, but Django's development server can be used as well:
```bash ```bash
cd panel cd panel
python3 manage.py migrate
python3 manage.py runserver 0.0.0.0:8888 python3 manage.py runserver 0.0.0.0:8888
``` ```
@ -34,5 +35,5 @@ If you are planing to do some development you may want to consider doing it unde
5. To upgrade to current master just re-run the deploy script 5. To upgrade to current master just re-run the deploy script
```bash ```bash
git pull origin master git pull origin master
bash <( curl -L http://git.io/orchestra-deploy ) --dev bash <( curl -L https://raw.githubusercontent.com/ribaguifi/django-orchestra/master/scripts/containers/deploy.sh ) --dev
``` ```

132
install_manually.md Normal file
View file

@ -0,0 +1,132 @@
# System requirements:
The most important requirement is use python3.6
we need install this packages:
```
bind9utils
ca-certificates
gettext
libcrack2-dev
libxml2-dev
libxslt1-dev
python3
python3-pip
python3-dev
ssh-client
wget
xvfb
zlib1g-dev
git
iceweasel
dnsutils
```
We need install too a *wkhtmltopdf* package
You can use one of your OS or get it from original.
This it is in https://wkhtmltopdf.org/downloads.html
# pip installations
We need install this packages:
```
Django==1.10.5
django-fluent-dashboard==0.6.1
django-admin-tools==0.8.0
django-extensions==1.7.4
django-celery==3.1.17
celery==3.1.23
kombu==3.0.35
billiard==3.3.0.23
Markdown==2.4
djangorestframework==3.4.7
ecdsa==0.11
Pygments==1.6
django-filter==0.15.2
jsonfield==0.9.22
python-dateutil==2.2
https://github.com/glic3rinu/passlib/archive/master.zip
django-iban==0.3.0
requests
phonenumbers
django-countries
django-localflavor
amqp
anyjson
pytz
cracklib
lxml==3.3.5
selenium
xvfbwrapper
freezegun
coverage
flake8
django-debug-toolbar==1.3.0
django-nose==1.4.4
sqlparse
pyinotify
PyMySQL
```
If you want to use Orchestra you need to install from pip like this:
```
pip3 install http://git.io/django-orchestra-dev
```
But if you want develop orquestra you need to do this:
```
git clone https://github.com/ribaguifi/django-orchestra
pip install -e django-orchestra
```
# Database
For default use sqlite3 if you want to use postgresql you need install this packages:
```
psycopg2 postgresql
```
You can use it for debian or ubuntu:
```
sudo apt-get install python3-psycopg2 postgresql-contrib
```
Remember create a database for your project and give permitions for the correct user like this:
```
psql -U postgres
psql (12.4)
Digite «help» para obtener ayuda.
postgres=# CREATE database orchesta;
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';
postgres=# GRANT ALL PRIVILEGES ON DATABASE orchesta TO orchesta;
```
# Create new project
You can use orchestra-admin for create your new project
```
orchestra-admin startproject <project_name> # e.g. panel
cd <project_name>
```
Next we need change the settings.py for configure the correct database
In settings.py we need change the DATABASE section like this:
```
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'orchestra'
'USER': 'orchestra',
'PASSWORD': 'orchestra',
'HOST': 'localhost',
'PORT': '5432',
'CONN_MAX_AGE': 60*10
}
}
```
For end you need to do the migrations:
```
python3 manage.py migrate
```

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -157,7 +157,7 @@ function install_requirements () {
PIP="${PIP} \ PIP="${PIP} \
selenium \ selenium \
xvfbwrapper \ xvfbwrapper \
freezegun \ freezegun==0.3.14 \
coverage \ coverage \
flake8 \ flake8 \
django-debug-toolbar==1.3.0 \ django-debug-toolbar==1.3.0 \
@ -174,7 +174,7 @@ function install_requirements () {
minor=$(echo -e "$wkhtmltox_version\n0.12.2.1" | sort -V | head -n 1) minor=$(echo -e "$wkhtmltox_version\n0.12.2.1" | sort -V | head -n 1)
if [[ ! $wkhtmltox_version ]] || [[ $wkhtmltox_version != 0.12.2.1 && $minor == ${wkhtmltox_version} ]]; then if [[ ! $wkhtmltox_version ]] || [[ $wkhtmltox_version != 0.12.2.1 && $minor == ${wkhtmltox_version} ]]; then
wkhtmltox=$(mktemp) wkhtmltox=$(mktemp)
wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb -O ${wkhtmltox} wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb -O ${wkhtmltox}
dpkg -i ${wkhtmltox} || { echo "Installing missing dependencies for wkhtmltox..." && apt-get -f -y install; } dpkg -i ${wkhtmltox} || { echo "Installing missing dependencies for wkhtmltox..." && apt-get -f -y install; }
fi fi
} }

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:08
from __future__ import unicode_literals
import django.contrib.auth.models
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import orchestra.contrib.accounts.models
class Migration(migrations.Migration):
replaces = [('accounts', '0001_initial'), ('accounts', '0002_auto_20170528_2005'), ('accounts', '0003_auto_20210330_1049'), ('accounts', '0004_auto_20210422_1108')]
initial = True
dependencies = [
('systemusers', '0001_initial'),
('auth', '0006_require_contenttypes_0002'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('username', models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username')),
('short_name', models.CharField(blank=True, max_length=64, verbose_name='short name')),
('full_name', models.CharField(max_length=256, verbose_name='full name')),
('email', models.EmailField(help_text='Used for password recovery', max_length=254, verbose_name='email address')),
('type', models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type')),
('language', models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language')),
('comments', models.TextField(blank=True, max_length=256, verbose_name='comments')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('main_systemuser', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main', to='systemusers.SystemUser')),
],
options={
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.AlterModelManagers(
name='account',
managers=[
('objects', orchestra.contrib.accounts.models.AccountManager()),
],
),
migrations.AlterField(
model_name='account',
name='language',
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
),
migrations.AlterField(
model_name='account',
name='type',
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='account',
name='username',
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username'),
),
migrations.AlterField(
model_name='account',
name='language',
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
),
migrations.AlterField(
model_name='account',
name='type',
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='account',
name='main_systemuser',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts_main', to='systemusers.SystemUser'),
),
]

View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:05
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import orchestra.contrib.accounts.models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
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'),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-03-30 10:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_auto_20170528_2005'),
]
operations = [
migrations.AlterField(
model_name='account',
name='language',
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
),
migrations.AlterField(
model_name='account',
name='type',
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
),
]

View file

@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
#from orchestra.contrib.orchestration.middlewares import OperationsMiddleware #from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
#from orchestra.contrib.orchestration import Operation #from orchestra.contrib.orchestration import Operation
from orchestra.core import services from orchestra import core
from orchestra.models.utils import has_db_field from orchestra.models.utils import has_db_field
from orchestra.utils.mail import send_email_template from orchestra.utils.mail import send_email_template
@ -29,7 +29,7 @@ class Account(auth.AbstractBaseUser):
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid') validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
]) ])
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True, main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
related_name='accounts_main', editable=False) related_name='accounts_main', editable=False, on_delete=models.SET_NULL)
short_name = models.CharField(_("short name"), max_length=64, blank=True) short_name = models.CharField(_("short name"), max_length=64, blank=True)
full_name = models.CharField(_("full name"), max_length=256) full_name = models.CharField(_("full name"), max_length=256)
email = models.EmailField(_('email address'), help_text=_("Used for password recovery")) email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
@ -52,6 +52,11 @@ class Account(auth.AbstractBaseUser):
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email'] REQUIRED_FIELDS = ['email']
def __init__(self, *args, **kwargs):
# ignore `is_staff` kwarg because is handled with `is_superuser`
kwargs.pop('is_staff', None)
super().__init__(*args, **kwargs)
def __str__(self): def __str__(self):
return self.name return self.name
@ -98,7 +103,7 @@ class Account(auth.AbstractBaseUser):
] ]
for rel in related_fields: for rel in related_fields:
source = getattr(rel, 'related_model', rel.model) source = getattr(rel, 'related_model', rel.model)
if source in services and hasattr(source, 'active'): if source in core.services and hasattr(source, 'active'):
for obj in getattr(self, rel.get_accessor_name()).all(): for obj in getattr(self, rel.get_accessor_name()).all():
yield obj yield obj
@ -141,12 +146,25 @@ class Account(auth.AbstractBaseUser):
backend returns True. Thus, a user who has permission from a single backend returns True. Thus, a user who has permission from a single
auth backend is assumed to have permission in general. If an object is auth backend is assumed to have permission in general. If an object is
provided, permissions for this specific object are checked. provided, permissions for this specific object are checked.
applabel.action_modelname
""" """
if not self.is_active:
return False
# Active superusers have all permissions. # Active superusers have all permissions.
if self.is_active and self.is_superuser: if self.is_superuser:
return True return True
# Otherwise we need to check the backends. app, action_model = perm.split('.')
return auth._user_has_perm(self, perm, obj) action, model = action_model.split('_', 1)
service_apps = set(model._meta.app_label for model in core.services.get().keys())
accounting_apps = set(model._meta.app_label for model in core.accounts.get().keys())
import inspect
if ((app in service_apps or (action == 'view' and app in accounting_apps))):
# class-level permissions
if inspect.isclass(obj):
return True
elif obj and getattr(obj, 'account', None) == self:
return True
def has_perms(self, perm_list, obj=None): def has_perms(self, perm_list, obj=None):
""" """
@ -167,7 +185,6 @@ class Account(auth.AbstractBaseUser):
# Active superusers have all permissions. # Active superusers have all permissions.
if self.is_active and self.is_superuser: if self.is_active and self.is_superuser:
return True return True
return auth._user_has_module_perms(self, app_label)
def get_related_passwords(self, db_field=False): def get_related_passwords(self, db_field=False):
related = [ related = [

View file

@ -7,7 +7,7 @@ class AccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Account model = Account
fields = ( fields = (
'url', 'id', 'username', 'type', 'language', 'short_name', 'full_name', 'date_joined', 'url', 'id', 'username', 'type', 'language', 'short_name', 'full_name', 'date_joined', 'last_login',
'is_active' 'is_active'
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-29 10:51+0000\n" "POT-Creation-Date: 2019-12-20 11:56+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,33 +18,33 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:31 #: actions.py:33
msgid "View" msgid "View"
msgstr "Vista" msgstr "Vista"
#: actions.py:42 #: actions.py:45
msgid "Selected bills should be in open state" msgid "Selected bills should be in open state"
msgstr "Les factures seleccionades han d'estar en estat obert" msgstr "Les factures seleccionades han d'estar en estat obert"
#: actions.py:57 #: actions.py:60
msgid "Selected bills have been closed" msgid "Selected bills have been closed"
msgstr "Les factures seleccionades han estat tancades" msgstr "Les factures seleccionades han estat tancades"
#: actions.py:70 #: actions.py:73
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One related transaction</a> has been created" msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>" msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
#: actions.py:71 #: actions.py:74
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created" msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>" msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
#: actions.py:77 #: actions.py:80
msgid "Are you sure about closing the following bills?" msgid "Are you sure about closing the following bills?"
msgstr "Estàs a punt de tancar les següents factures, estàs segur?" msgstr "Estàs a punt de tancar les següents factures, estàs segur?"
#: actions.py:78 #: actions.py:81
msgid "" msgid ""
"Once a bill is closed it can not be further modified.</p><p>Please select a " "Once a bill is closed it can not be further modified.</p><p>Please select a "
"payment source for the selected bills" "payment source for the selected bills"
@ -52,174 +52,205 @@ msgstr ""
"Una vegada la factura estigui tancada no podrà ser modificada.</p><p>Si us " "Una vegada la factura estigui tancada no podrà ser modificada.</p><p>Si us "
"plau selecciona un mètode de pagament per les factures seleccionades" "plau selecciona un mètode de pagament per les factures seleccionades"
#: actions.py:91 #: actions.py:97
msgid "Close" msgid "Close"
msgstr "Tanca" msgstr "Tanca"
#: actions.py:109 #: actions.py:115
msgid "One bill has been sent." msgid "One bill has been sent."
msgstr "S'ha creat una factura" msgstr "S'ha creat una factura"
#: actions.py:110 #: actions.py:116
#, python-format #, python-format
msgid "%i bills have been sent." msgid "%i bills have been sent."
msgstr "S'han enviat %i factures." msgstr "S'han enviat %i factures."
#: actions.py:117 #: actions.py:123
msgid "Resend" msgid "Resend"
msgstr "Reenviat" msgstr "Reenviat"
#: actions.py:137 #: actions.py:146
msgid "Download" msgid "Download"
msgstr "Descarrega" msgstr "Descarrega"
#: actions.py:153 #: actions.py:162
msgid "C.S.D." msgid "C.S.D."
msgstr "" msgstr ""
#: actions.py:155 #: actions.py:164
msgid "Close, send and download bills in one shot." msgid "Close, send and download bills in one shot."
msgstr "" msgstr ""
#: actions.py:216 #: actions.py:225
#, python-format #, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed." msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr "%(norders)s ordres i %(nlines)s línies desfetes." msgstr "%(norders)s ordres i %(nlines)s línies desfetes."
#: actions.py:235 #: actions.py:244
msgid "Lines moved" msgid "Lines moved"
msgstr "Línies mogudes" msgstr "Línies mogudes"
#: actions.py:248 #: actions.py:257
msgid "Selected bills should be in closed state" msgid "Selected bills should be in closed state"
msgstr "Les factures seleccionades han d'estar en estat obert" msgstr "Les factures seleccionades han d'estar en estat obert"
#: actions.py:265 #: actions.py:259
#, python-format
msgid "%s can not be amended."
msgstr ""
#: actions.py:279
#, python-format #, python-format
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s" msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
msgstr "%(type)s de %(related_type)s %(number)s amb data de creació %(date)s" msgstr "%(type)s de %(related_type)s %(number)s amb data de creació %(date)s"
#: actions.py:272 #: actions.py:286
#, python-format #, python-format
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%" msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%" msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
#: actions.py:288 #: actions.py:303
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated." msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>" msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
#: actions.py:289 #: actions.py:304
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated." msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>" msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
#: actions.py:292 #: actions.py:307
msgid "Amend" msgid "Amend"
msgstr "" msgstr ""
#: admin.py:58 admin.py:103 admin.py:140 forms.py:11 #: admin.py:80 admin.py:126 admin.py:180 forms.py:11
#: templates/admin/bills/bill/report.html:43 #: templates/admin/bills/bill/report.html:43
#: templates/admin/bills/bill/report.html:70 #: templates/admin/bills/bill/report.html:70
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#: admin.py:89 #: admin.py:112
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: admin.py:97 #: admin.py:120
msgid "Subtotal" msgid "Subtotal"
msgstr "Subtotal" msgstr "Subtotal"
#: admin.py:130 #: admin.py:146
#, fuzzy
#| msgid "Total"
msgid "Totals"
msgstr "Total"
#: admin.py:150
msgid "Order"
msgstr ""
#: admin.py:169
msgid "Is open" msgid "Is open"
msgstr "És oberta" msgstr "És oberta"
#: admin.py:135 #: admin.py:175
msgid "Subline" #, fuzzy
#| msgid "Subline"
msgid "Sublines"
msgstr "Sublínia" msgstr "Sublínia"
#: admin.py:167 #: admin.py:221
msgid "No bills selected." msgid "No bills selected."
msgstr "No hi ha factures seleccionades" msgstr "No hi ha factures seleccionades"
#: admin.py:174 #: admin.py:229
#, python-format #, fuzzy, python-format
msgid "Manage %s bill lines." #| msgid "Manage %s bill lines."
msgid "Manage %s bill lines"
msgstr "Gestiona %s línies de factura." msgstr "Gestiona %s línies de factura."
#: admin.py:176 #: admin.py:231
msgid "Bill not in open state." msgid "Bill not in open state."
msgstr "La factura no està en estat obert" msgstr "La factura no està en estat obert"
#: admin.py:179 #: admin.py:234
msgid "Not all bills are in open state." msgid "Not all bills are in open state."
msgstr "No totes les factures estan en estat obert" msgstr "No totes les factures estan en estat obert"
#: admin.py:180 #: admin.py:235
msgid "Manage bill lines of multiple bills." #, fuzzy
#| msgid "Manage bill lines of multiple bills."
msgid "Manage bill lines of multiple bills"
msgstr "Gestiona línies de factura de multiples factures." msgstr "Gestiona línies de factura de multiples factures."
#: admin.py:204 #: admin.py:250
msgid "Dates" #, python-format
msgid "Subtotal %s%% VAT %s &%s;"
msgstr "" msgstr ""
#: admin.py:209 #: admin.py:251
msgid "Raw" #, python-format
msgstr "Raw" msgid "Taxes %s%% VAT %s &%s;"
msgstr ""
#: admin.py:235 models.py:73 #: admin.py:255 admin.py:381 filters.py:46
msgid "Created" #: templates/bills/microspective.html:123
msgstr "Creada" msgid "total"
msgstr "total"
#: admin.py:236 #: admin.py:275
#, fuzzy msgid "This bill has been amended, this value may not be valid."
#| msgid "Close" msgstr ""
msgid "Closed"
msgstr "Tanca"
#: admin.py:237 #: admin.py:280
#, fuzzy msgid "Payment"
#| msgid "updated on" msgstr "Pagament"
msgid "Updated"
msgstr "actualitzada el"
#: admin.py:246 #: admin.py:304
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "Amends" msgid "Amends"
msgstr "línia rectificada" msgstr "línia rectificada"
#: admin.py:252 #: admin.py:330
msgid "Dates"
msgstr ""
#: admin.py:335
msgid "Raw"
msgstr "Raw"
#: admin.py:358 models.py:75
msgid "Created"
msgstr "Creada"
#: admin.py:359
#, fuzzy
#| msgid "Close"
msgid "Closed"
msgstr "Tanca"
#: admin.py:360
#, fuzzy
#| msgid "updated on"
msgid "Updated"
msgstr "actualitzada el"
#: admin.py:375
msgid "lines" msgid "lines"
msgstr "línies" msgstr "línies"
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118 #: admin.py:389 models.py:108 models.py:501
msgid "total"
msgstr "total"
#: admin.py:265 models.py:104 models.py:460
msgid "type" msgid "type"
msgstr "tipus" msgstr "tipus"
#: admin.py:282
msgid "This bill has been amended, this value may not be valid."
msgstr ""
#: admin.py:287
msgid "Payment"
msgstr "Pagament"
#: filters.py:21 #: filters.py:21
msgid "All" msgid "All"
msgstr "Tot" msgstr "Tot"
#: filters.py:22 models.py:88 #: filters.py:22 models.py:91
msgid "Invoice" msgid "Invoice"
msgstr "Factura" msgstr "Factura"
#: filters.py:23 models.py:90 #: filters.py:23 models.py:93
msgid "Fee" msgid "Fee"
msgstr "Quota de soci" msgstr "Quota de soci"
@ -231,65 +262,67 @@ msgstr "Pro-forma"
msgid "Amendment fee" msgid "Amendment fee"
msgstr "Rectificació de quota de soci" msgstr "Rectificació de quota de soci"
#: filters.py:26 models.py:89 #: filters.py:26 models.py:92
msgid "Amendment invoice" msgid "Amendment invoice"
msgstr "Factura rectificativa" msgstr "Factura rectificativa"
#: filters.py:68 #: filters.py:71
msgid "has bill contact" msgid "has bill contact"
msgstr "té contacte de facturació" msgstr "té contacte de facturació"
#: filters.py:73 #: filters.py:76
msgid "Yes" msgid "Yes"
msgstr "Si" msgstr "Si"
#: filters.py:74 #: filters.py:77
msgid "No" msgid "No"
msgstr "No" msgstr "No"
#: filters.py:85 #: filters.py:88
msgid "payment state" msgid "payment state"
msgstr "Pagament" msgstr "Pagament"
#: filters.py:90 models.py:72 #: filters.py:93 models.py:74
msgid "Open" msgid "Open"
msgstr "" msgstr ""
#: filters.py:91 models.py:76 #: filters.py:94 models.py:78
msgid "Paid" msgid "Paid"
msgstr "Pagat" msgstr "Pagat"
#: filters.py:92 #: filters.py:95
msgid "Pending" msgid "Pending"
msgstr "Pendent" msgstr "Pendent"
#: filters.py:93 models.py:79 #: filters.py:96 models.py:81
msgid "Bad debt" msgid "Bad debt"
msgstr "Incobrable" msgstr "Incobrable"
#: filters.py:135 #: filters.py:138
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "amended" msgid "amended"
msgstr "línia rectificada" msgstr "línia rectificada"
#: filters.py:140 #: filters.py:143
#, fuzzy #, fuzzy
#| msgid "Due date" #| msgid "Due date"
msgid "Closed amends" msgid "Closed amends"
msgstr "Data de pagament" msgstr "Data de pagament"
#: filters.py:141 #: filters.py:144
msgid "Open or closed amends"
msgstr ""
#: filters.py:142
#, fuzzy #, fuzzy
#| msgid "closed on" #| msgid "Due date"
msgid "No closed amends" msgid "Open amends"
msgstr "tancat el" msgstr "Data de pagament"
#: filters.py:143 #: filters.py:145
#, fuzzy
#| msgid "amended line"
msgid "Any amends"
msgstr "línia rectificada"
#: filters.py:146
msgid "No amends" msgid "No amends"
msgstr "" msgstr ""
@ -309,7 +342,7 @@ msgstr "Tipus"
msgid "Source" msgid "Source"
msgstr "Font" msgstr "Font"
#: helpers.py:10 #: helpers.py:14
msgid "" msgid ""
"{relation} account \"{account}\" does not have a declared invoice contact. " "{relation} account \"{account}\" does not have a declared invoice contact. "
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>" "You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
@ -317,213 +350,235 @@ msgstr ""
"{relation} compte \"{account}\" no te un contacte de facturació. Hauries de " "{relation} compte \"{account}\" no te un contacte de facturació. Hauries de "
"<a href=\"{url}#invoicecontact-group\">proporcionar un</a>" "<a href=\"{url}#invoicecontact-group\">proporcionar un</a>"
#: helpers.py:17 #: helpers.py:21
msgid "Related" msgid "Related"
msgstr "Relacionat" msgstr "Relacionat"
#: helpers.py:24 #: helpers.py:28
msgid "Main" msgid "Main"
msgstr "Principal" msgstr "Principal"
#: models.py:24 models.py:100 #: models.py:26 models.py:104
msgid "account" msgid "account"
msgstr "compte" msgstr "compte"
#: models.py:26 #: models.py:28
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
#: models.py:27 #: models.py:29
msgid "Account full name will be used when left blank." msgid "Account full name will be used when left blank."
msgstr "S'emprarà el nom complet del compte quan es deixi en blanc." msgstr "S'emprarà el nom complet del compte quan es deixi en blanc."
#: models.py:28 #: models.py:30
msgid "address" msgid "address"
msgstr "adreça" msgstr "adreça"
#: models.py:29 #: models.py:31
msgid "city" msgid "city"
msgstr "ciutat" msgstr "ciutat"
#: models.py:31 #: models.py:33
msgid "zip code" msgid "zip code"
msgstr "codi postal" msgstr "codi postal"
#: models.py:32 #: models.py:34
msgid "Enter a valid zipcode." msgid "Enter a valid zipcode."
msgstr "Introdueix un codi postal vàlid." msgstr "Introdueix un codi postal vàlid."
#: models.py:33 #: models.py:35
msgid "country" msgid "country"
msgstr "país" msgstr "país"
#: models.py:36 templates/admin/bills/bill/report.html:65 #: models.py:38 templates/admin/bills/bill/report.html:65
msgid "VAT number" msgid "VAT number"
msgstr "NIF" msgstr "NIF"
#: models.py:74 #: models.py:76
msgid "Processed" msgid "Processed"
msgstr "" msgstr ""
#: models.py:75 #: models.py:77
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "Amended" msgid "Amended"
msgstr "línia rectificada" msgstr "línia rectificada"
#: models.py:77 #: models.py:79
msgid "Incomplete" msgid "Incomplete"
msgstr "" msgstr ""
#: models.py:78 #: models.py:80
msgid "Executed" msgid "Executed"
msgstr "" msgstr ""
#: models.py:91 #: models.py:94
msgid "Amendment Fee" msgid "Amendment Fee"
msgstr "Rectificació de quota de soci" msgstr "Rectificació de quota de soci"
#: models.py:92 #: models.py:95
#, fuzzy
#| msgid "Invoice"
msgid "Abono Invoice"
msgstr "Abonament"
#: models.py:96
msgid "Pro forma" msgid "Pro forma"
msgstr "Pro forma" msgstr "Pro forma"
#: models.py:99 #: models.py:103
msgid "number" msgid "number"
msgstr "número" msgstr "número"
#: models.py:102 #: models.py:106
#, fuzzy #, fuzzy
#| msgid "amended line" #| msgid "amended line"
msgid "amend of" msgid "amend of"
msgstr "línia rectificada" msgstr "línia rectificada"
#: models.py:105 #: models.py:109
msgid "created on" msgid "created on"
msgstr "creat el" msgstr "creat el"
#: models.py:106 #: models.py:110
msgid "closed on" msgid "closed on"
msgstr "tancat el" msgstr "tancat el"
#: models.py:107 #: models.py:111
msgid "open" msgid "open"
msgstr "obert" msgstr "obert"
#: models.py:108 #: models.py:112
msgid "sent" msgid "sent"
msgstr "enviat" msgstr "enviat"
#: models.py:109 #: models.py:113
msgid "due on" msgid "due on"
msgstr "es deu" msgstr "es deu"
#: models.py:110 #: models.py:114
msgid "updated on" msgid "updated on"
msgstr "actualitzada el" msgstr "actualitzada el"
#: models.py:112 #: models.py:116
msgid "comments" msgid "comments"
msgstr "comentaris" msgstr "comentaris"
#: models.py:113 #: models.py:117
msgid "HTML" msgid "HTML"
msgstr "HTML" msgstr "HTML"
#: models.py:194 #: models.py:200
#, python-format #, python-format
msgid "Type %s is not an amendment." msgid "Type %s is not an amendment."
msgstr "" msgstr ""
#: models.py:196 #: models.py:202
msgid "Amend of related account doesn't match bill account." msgid "Amend of related account doesn't match bill account."
msgstr "" msgstr ""
#: models.py:198 #: models.py:204
#, fuzzy #, fuzzy
#| msgid "Bill not in open state." #| msgid "Bill not in open state."
msgid "Related invoice is in open state." msgid "Related invoice is in open state."
msgstr "La factura no està en estat obert" msgstr "La factura no està en estat obert"
#: models.py:200 #: models.py:206
msgid "Related invoice is an amendment." msgid "Related invoice is an amendment."
msgstr "" msgstr ""
#: models.py:392 #: models.py:419
msgid "bill" msgid "bill"
msgstr "factura" msgstr "factura"
#: models.py:393 models.py:458 templates/bills/microspective.html:73 #: models.py:420 models.py:499 templates/bills/microspective.html:75
msgid "description" msgid "description"
msgstr "descripció" msgstr "descripció"
#: models.py:394 #: models.py:421
msgid "rate" msgid "rate"
msgstr "tarifa" msgstr "tarifa"
#: models.py:395 #: models.py:422
msgid "quantity" msgid "quantity"
msgstr "quantitat" msgstr "quantitat"
#: models.py:397 #: models.py:424
#, fuzzy #, fuzzy
#| msgid "quantity" #| msgid "quantity"
msgid "Verbose quantity" msgid "Verbose quantity"
msgstr "quantitat" msgstr "quantitat"
#: models.py:398 templates/admin/bills/bill/report.html:47 #: models.py:425 templates/admin/bills/bill/report.html:47
#: templates/bills/microspective.html:77 #: templates/bills/microspective.html:79
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
msgid "subtotal" msgid "subtotal"
msgstr "subtotal" msgstr "subtotal"
#: models.py:399 #: models.py:426
msgid "tax" msgid "tax"
msgstr "impostos" msgstr "impostos"
#: models.py:400 #: models.py:427
msgid "start" msgid "start"
msgstr "iniciar" msgstr "iniciar"
#: models.py:401 #: models.py:428
msgid "end" msgid "end"
msgstr "finalitzar" msgstr "finalitzar"
#: models.py:403 #: models.py:431
msgid "Informative link back to the order" msgid "Informative link back to the order"
msgstr "Enllaç informatiu de l'ordre" msgstr "Enllaç informatiu de l'ordre"
#: models.py:404 #: models.py:432
msgid "order billed" msgid "order billed"
msgstr "ordre facturada" msgstr "ordre facturada"
#: models.py:405 #: models.py:433
msgid "order billed until" msgid "order billed until"
msgstr "ordre facturada fins a" msgstr "ordre facturada fins a"
#: models.py:406 #: models.py:434
msgid "created" msgid "created"
msgstr "creada" msgstr "creada"
#: models.py:408 #: models.py:436
msgid "amended line" msgid "amended line"
msgstr "línia rectificada" msgstr "línia rectificada"
#: models.py:451 #: models.py:492
msgid "Volume" msgid "Volume"
msgstr "Volum" msgstr "Volum"
#: models.py:452 #: models.py:493
msgid "Compensation" msgid "Compensation"
msgstr "Compensació" msgstr "Compensació"
#: models.py:453 #: models.py:494
msgid "Other" msgid "Other"
msgstr "Altre" msgstr "Altre"
#: models.py:457 #: models.py:498
msgid "bill line" msgid "bill line"
msgstr "línia de factura" msgstr "línia de factura"
#: templates/admin/bills/bill/change_list.html:9
#, fuzzy
#| msgid "lines"
msgid "Lines"
msgstr "línies"
#: templates/admin/bills/bill/change_list.html:15
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/admin/bills/bill/close_send_download_bills.html:57
msgid "Yes, I'm sure"
msgstr ""
#: templates/admin/bills/bill/report.html:42 #: templates/admin/bills/bill/report.html:42
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
@ -531,19 +586,19 @@ msgstr ""
#: templates/admin/bills/bill/report.html:47 #: templates/admin/bills/bill/report.html:47
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/admin/bills/bill/report.html:69 #: templates/admin/bills/bill/report.html:69
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "VAT" msgid "VAT"
msgstr "IVA" msgstr "IVA"
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "taxes" msgid "taxes"
msgstr "impostos" msgstr "impostos"
#: templates/admin/bills/bill/report.html:56 #: templates/admin/bills/bill/report.html:56
#: templates/admin/bills/billline/report.html:60 #: templates/admin/bills/billline/report.html:60
#: templates/bills/microspective.html:53 #: templates/bills/microspective.html:54
msgid "TOTAL" msgid "TOTAL"
msgstr "TOTAL" msgstr "TOTAL"
@ -561,8 +616,20 @@ msgstr "Data de pagament"
msgid "Base" msgid "Base"
msgstr "" msgstr ""
#: templates/admin/bills/billline/change_list.html:6
msgid "Home"
msgstr ""
#: templates/admin/bills/billline/change_list.html:8
msgid "Bills"
msgstr ""
#: templates/admin/bills/billline/change_list.html:9
msgid "Multiple bills"
msgstr ""
#: templates/admin/bills/billline/report.html:42 #: templates/admin/bills/billline/report.html:42
msgid "Services" msgid "Service"
msgstr "" msgstr ""
#: templates/admin/bills/billline/report.html:43 #: templates/admin/bills/billline/report.html:43
@ -587,27 +654,21 @@ msgstr "quantitat"
msgid "Profit" msgid "Profit"
msgstr "" msgstr ""
#: templates/admin/bills/change_list.html:9 #: templates/bills/microspective-fee.html:115
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/bills/microspective-fee.html:107
msgid "Due date" msgid "Due date"
msgstr "Data de pagament" msgstr "Data de pagament"
#: templates/bills/microspective-fee.html:108 #: templates/bills/microspective-fee.html:116
#, python-format #, python-format
msgid "On %(bank_account)s" msgid "On %(bank_account)s"
msgstr "Al %(bank_account)s" msgstr "Al %(bank_account)s"
#: templates/bills/microspective-fee.html:114 #: templates/bills/microspective-fee.html:122
#, python-format #, python-format
msgid "From %(ini)s to %(end)s" msgid "From %(ini)s to %(end)s"
msgstr "De %(ini)s a %(end)s" msgstr "De %(ini)s a %(end)s"
#: templates/bills/microspective-fee.html:121 #: templates/bills/microspective-fee.html:144
msgid "" msgid ""
"\n" "\n"
"<strong>With your membership</strong> you are supporting ...\n" "<strong>With your membership</strong> you are supporting ...\n"
@ -615,36 +676,36 @@ msgstr ""
"\n" "\n"
"<strong>Amb la teva quota de soci</strong> estàs donant suport ...\n" "<strong>Amb la teva quota de soci</strong> estàs donant suport ...\n"
#: templates/bills/microspective.html:49 #: templates/bills/microspective.html:50
msgid "DUE DATE" msgid "DUE DATE"
msgstr "VENCIMENT" msgstr "VENCIMENT"
#: templates/bills/microspective.html:57 #: templates/bills/microspective.html:58
#, python-format #, python-format
msgid "%(bill_type)s DATE" msgid "%(bill_type)s DATE"
msgstr "DATA %(bill_type)s" msgstr "DATA %(bill_type)s"
#: templates/bills/microspective.html:74 #: templates/bills/microspective.html:76
msgid "period" msgid "period"
msgstr "període" msgstr "període"
#: templates/bills/microspective.html:75 #: templates/bills/microspective.html:77
msgid "hrs/qty" msgid "hrs/qty"
msgstr "hrs/qnt" msgstr "hrs/qnt"
#: templates/bills/microspective.html:76 #: templates/bills/microspective.html:78
msgid "rate/price" msgid "rate/price"
msgstr "tarifa/preu" msgstr "tarifa/preu"
#: templates/bills/microspective.html:131 #: templates/bills/microspective.html:137
msgid "COMMENTS" msgid "COMMENTS"
msgstr "COMENTARIS" msgstr "COMENTARIS"
#: templates/bills/microspective.html:138 #: templates/bills/microspective.html:145
msgid "PAYMENT" msgid "PAYMENT"
msgstr "PAGAMENT" msgstr "PAGAMENT"
#: templates/bills/microspective.html:142 #: templates/bills/microspective.html:149
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -658,11 +719,11 @@ msgstr ""
"Pots pagar aquesta <i>%(type)s</i> per transferència bancaria.<br>Inclou el " "Pots pagar aquesta <i>%(type)s</i> per transferència bancaria.<br>Inclou el "
"teu nom i el número de <i>%(type)s</i>. El nostre compte bancari és" "teu nom i el número de <i>%(type)s</i>. El nostre compte bancari és"
#: templates/bills/microspective.html:151 #: templates/bills/microspective.html:160
msgid "QUESTIONS" msgid "QUESTIONS"
msgstr "PREGUNTES" msgstr "PREGUNTES"
#: templates/bills/microspective.html:152 #: templates/bills/microspective.html:161
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -679,5 +740,10 @@ msgstr ""
"ràpidament possible.\n" "ràpidament possible.\n"
" " " "
#, fuzzy
#~| msgid "closed on"
#~ msgid "No closed amends"
#~ msgstr "tancat el"
#~ msgid "positive price" #~ msgid "positive price"
#~ msgstr "preu positiu" #~ msgstr "preu positiu"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-29 10:51+0000\n" "POT-Creation-Date: 2019-12-20 11:56+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,33 +18,33 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:31 #: actions.py:33
msgid "View" msgid "View"
msgstr "Vista" msgstr "Vista"
#: actions.py:42 #: actions.py:45
msgid "Selected bills should be in open state" msgid "Selected bills should be in open state"
msgstr "Las facturas seleccionadas están en estado abierto" msgstr "Las facturas seleccionadas están en estado abierto"
#: actions.py:57 #: actions.py:60
msgid "Selected bills have been closed" msgid "Selected bills have been closed"
msgstr "Las facturas seleccionadas han sido cerradas" msgstr "Las facturas seleccionadas han sido cerradas"
#: actions.py:70 #: actions.py:73
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One related transaction</a> has been created" msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>" msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
#: actions.py:71 #: actions.py:74
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created" msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>" msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
#: actions.py:77 #: actions.py:80
msgid "Are you sure about closing the following bills?" msgid "Are you sure about closing the following bills?"
msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?" msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?"
#: actions.py:78 #: actions.py:81
msgid "" msgid ""
"Once a bill is closed it can not be further modified.</p><p>Please select a " "Once a bill is closed it can not be further modified.</p><p>Please select a "
"payment source for the selected bills" "payment source for the selected bills"
@ -52,174 +52,199 @@ msgstr ""
"Una vez cerrada la factura ya no se podrá modificar.</p><p>Por favor " "Una vez cerrada la factura ya no se podrá modificar.</p><p>Por favor "
"seleciona un metodo de pago para las facturas seleccionadas" "seleciona un metodo de pago para las facturas seleccionadas"
#: actions.py:91 #: actions.py:97
msgid "Close" msgid "Close"
msgstr "Cerrar" msgstr "Cerrar"
#: actions.py:109 #: actions.py:115
msgid "One bill has been sent." msgid "One bill has been sent."
msgstr "Se ha enviado una factura" msgstr "Se ha enviado una factura"
#: actions.py:110 #: actions.py:116
#, python-format #, python-format
msgid "%i bills have been sent." msgid "%i bills have been sent."
msgstr "" msgstr ""
#: actions.py:117 #: actions.py:123
msgid "Resend" msgid "Resend"
msgstr "" msgstr ""
#: actions.py:137 #: actions.py:146
msgid "Download" msgid "Download"
msgstr "Descarga" msgstr "Descarga"
#: actions.py:153 #: actions.py:162
msgid "C.S.D." msgid "C.S.D."
msgstr "" msgstr ""
#: actions.py:155 #: actions.py:164
msgid "Close, send and download bills in one shot." msgid "Close, send and download bills in one shot."
msgstr "" msgstr ""
#: actions.py:216 #: actions.py:225
#, python-format #, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed." msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr "" msgstr ""
#: actions.py:235 #: actions.py:244
msgid "Lines moved" msgid "Lines moved"
msgstr "" msgstr ""
#: actions.py:248 #: actions.py:257
msgid "Selected bills should be in closed state" msgid "Selected bills should be in closed state"
msgstr "Las facturas seleccionadas están en estado abierto" msgstr "Las facturas seleccionadas están en estado abierto"
#: actions.py:265 #: actions.py:259
#, python-format
msgid "%s can not be amended."
msgstr ""
#: actions.py:279
#, python-format #, python-format
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s" msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
msgstr "%(type)s de %(related_type)s %(number)s con fecha de creación %(date)s" msgstr "%(type)s de %(related_type)s %(number)s con fecha de creación %(date)s"
#: actions.py:272 #: actions.py:286
#, python-format #, python-format
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%" msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%" msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
#: actions.py:288 #: actions.py:303
#, python-format #, python-format
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated." msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>" msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
#: actions.py:289 #: actions.py:304
#, python-format #, python-format
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated." msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>" msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
#: actions.py:292 #: actions.py:307
msgid "Amend" msgid "Amend"
msgstr "" msgstr ""
#: admin.py:58 admin.py:103 admin.py:140 forms.py:11 #: admin.py:80 admin.py:126 admin.py:180 forms.py:11
#: templates/admin/bills/bill/report.html:43 #: templates/admin/bills/bill/report.html:43
#: templates/admin/bills/bill/report.html:70 #: templates/admin/bills/bill/report.html:70
msgid "Total" msgid "Total"
msgstr "" msgstr ""
#: admin.py:89 #: admin.py:112
msgid "Description" msgid "Description"
msgstr "" msgstr ""
#: admin.py:97 #: admin.py:120
msgid "Subtotal" msgid "Subtotal"
msgstr "" msgstr ""
#: admin.py:130 #: admin.py:146
msgid "Totals"
msgstr ""
#: admin.py:150
msgid "Order"
msgstr ""
#: admin.py:169
msgid "Is open" msgid "Is open"
msgstr "" msgstr ""
#: admin.py:135 #: admin.py:175
msgid "Subline" msgid "Sublines"
msgstr "" msgstr ""
#: admin.py:167 #: admin.py:221
msgid "No bills selected." msgid "No bills selected."
msgstr "" msgstr ""
#: admin.py:174 #: admin.py:229
#, python-format #, fuzzy, python-format
msgid "Manage %s bill lines." #| msgid "bill line"
msgstr "" msgid "Manage %s bill lines"
msgstr "linea de factura"
#: admin.py:176 #: admin.py:231
msgid "Bill not in open state." msgid "Bill not in open state."
msgstr "" msgstr ""
#: admin.py:179 #: admin.py:234
msgid "Not all bills are in open state." msgid "Not all bills are in open state."
msgstr "" msgstr ""
#: admin.py:180 #: admin.py:235
msgid "Manage bill lines of multiple bills." msgid "Manage bill lines of multiple bills"
msgstr "" msgstr ""
#: admin.py:204 #: admin.py:250
msgid "Dates" #, python-format
msgid "Subtotal %s%% VAT %s &%s;"
msgstr "" msgstr ""
#: admin.py:209 #: admin.py:251
msgid "Raw" #, python-format
msgid "Taxes %s%% VAT %s &%s;"
msgstr "" msgstr ""
#: admin.py:235 models.py:73 #: admin.py:255 admin.py:381 filters.py:46
msgid "Created" #: templates/bills/microspective.html:123
msgid "total"
msgstr "" msgstr ""
#: admin.py:236 #: admin.py:275
#, fuzzy msgid "This bill has been amended, this value may not be valid."
#| msgid "Close" msgstr ""
msgid "Closed"
msgstr "Cerrar"
#: admin.py:237 #: admin.py:280
#, fuzzy msgid "Payment"
#| msgid "updated on" msgstr "Pago"
msgid "Updated"
msgstr "actualizada en"
#: admin.py:246 #: admin.py:304
#, fuzzy #, fuzzy
#| msgid "Amended" #| msgid "Amended"
msgid "Amends" msgid "Amends"
msgstr "Quota rectificativa" msgstr "Quota rectificativa"
#: admin.py:252 #: admin.py:330
msgid "Dates"
msgstr ""
#: admin.py:335
msgid "Raw"
msgstr ""
#: admin.py:358 models.py:75
msgid "Created"
msgstr ""
#: admin.py:359
#, fuzzy
#| msgid "Close"
msgid "Closed"
msgstr "Cerrar"
#: admin.py:360
#, fuzzy
#| msgid "updated on"
msgid "Updated"
msgstr "actualizada en"
#: admin.py:375
msgid "lines" msgid "lines"
msgstr "" msgstr ""
#: admin.py:257 filters.py:46 templates/bills/microspective.html:118 #: admin.py:389 models.py:108 models.py:501
msgid "total"
msgstr ""
#: admin.py:265 models.py:104 models.py:460
msgid "type" msgid "type"
msgstr "" msgstr ""
#: admin.py:282
msgid "This bill has been amended, this value may not be valid."
msgstr ""
#: admin.py:287
msgid "Payment"
msgstr "Pago"
#: filters.py:21 #: filters.py:21
msgid "All" msgid "All"
msgstr "" msgstr ""
#: filters.py:22 models.py:88 #: filters.py:22 models.py:91
msgid "Invoice" msgid "Invoice"
msgstr "Factura" msgstr "Factura"
#: filters.py:23 models.py:90 #: filters.py:23 models.py:93
msgid "Fee" msgid "Fee"
msgstr "Cuota de socio" msgstr "Cuota de socio"
@ -231,65 +256,67 @@ msgstr ""
msgid "Amendment fee" msgid "Amendment fee"
msgstr "Cuota rectificativa" msgstr "Cuota rectificativa"
#: filters.py:26 models.py:89 #: filters.py:26 models.py:92
msgid "Amendment invoice" msgid "Amendment invoice"
msgstr "Factura rectificativa" msgstr "Factura rectificativa"
#: filters.py:68 #: filters.py:71
msgid "has bill contact" msgid "has bill contact"
msgstr "" msgstr ""
#: filters.py:73 #: filters.py:76
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: filters.py:74 #: filters.py:77
msgid "No" msgid "No"
msgstr "" msgstr ""
#: filters.py:85 #: filters.py:88
msgid "payment state" msgid "payment state"
msgstr "Pago" msgstr "Pago"
#: filters.py:90 models.py:72 #: filters.py:93 models.py:74
msgid "Open" msgid "Open"
msgstr "" msgstr ""
#: filters.py:91 models.py:76 #: filters.py:94 models.py:78
msgid "Paid" msgid "Paid"
msgstr "" msgstr ""
#: filters.py:92 #: filters.py:95
msgid "Pending" msgid "Pending"
msgstr "" msgstr ""
#: filters.py:93 models.py:79 #: filters.py:96 models.py:81
msgid "Bad debt" msgid "Bad debt"
msgstr "" msgstr ""
#: filters.py:135 #: filters.py:138
#, fuzzy #, fuzzy
#| msgid "Amended" #| msgid "Amended"
msgid "amended" msgid "amended"
msgstr "Quota rectificativa" msgstr "Quota rectificativa"
#: filters.py:140 #: filters.py:143
#, fuzzy #, fuzzy
#| msgid "Due date" #| msgid "Due date"
msgid "Closed amends" msgid "Closed amends"
msgstr "Fecha de pago" msgstr "Fecha de pago"
#: filters.py:141 #: filters.py:144
msgid "Open or closed amends"
msgstr ""
#: filters.py:142
#, fuzzy #, fuzzy
#| msgid "closed on" #| msgid "Due date"
msgid "No closed amends" msgid "Open amends"
msgstr "cerrada en" msgstr "Fecha de pago"
#: filters.py:143 #: filters.py:145
#, fuzzy
#| msgid "Amended"
msgid "Any amends"
msgstr "Quota rectificativa"
#: filters.py:146
msgid "No amends" msgid "No amends"
msgstr "" msgstr ""
@ -309,213 +336,233 @@ msgstr ""
msgid "Source" msgid "Source"
msgstr "" msgstr ""
#: helpers.py:10 #: helpers.py:14
msgid "" msgid ""
"{relation} account \"{account}\" does not have a declared invoice contact. " "{relation} account \"{account}\" does not have a declared invoice contact. "
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>" "You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
msgstr "" msgstr ""
#: helpers.py:17 #: helpers.py:21
msgid "Related" msgid "Related"
msgstr "" msgstr ""
#: helpers.py:24 #: helpers.py:28
msgid "Main" msgid "Main"
msgstr "" msgstr ""
#: models.py:24 models.py:100 #: models.py:26 models.py:104
msgid "account" msgid "account"
msgstr "" msgstr ""
#: models.py:26 #: models.py:28
msgid "name" msgid "name"
msgstr "" msgstr ""
#: models.py:27 #: models.py:29
msgid "Account full name will be used when left blank." msgid "Account full name will be used when left blank."
msgstr "" msgstr ""
#: models.py:28 #: models.py:30
msgid "address" msgid "address"
msgstr "" msgstr ""
#: models.py:29 #: models.py:31
msgid "city" msgid "city"
msgstr "" msgstr ""
#: models.py:31 #: models.py:33
msgid "zip code" msgid "zip code"
msgstr "" msgstr ""
#: models.py:32 #: models.py:34
msgid "Enter a valid zipcode." msgid "Enter a valid zipcode."
msgstr "" msgstr ""
#: models.py:33 #: models.py:35
msgid "country" msgid "country"
msgstr "" msgstr ""
#: models.py:36 templates/admin/bills/bill/report.html:65 #: models.py:38 templates/admin/bills/bill/report.html:65
msgid "VAT number" msgid "VAT number"
msgstr "" msgstr ""
#: models.py:74 #: models.py:76
msgid "Processed" msgid "Processed"
msgstr "" msgstr ""
#: models.py:75 #: models.py:77
msgid "Amended" msgid "Amended"
msgstr "Quota rectificativa" msgstr "Quota rectificativa"
#: models.py:77 #: models.py:79
msgid "Incomplete" msgid "Incomplete"
msgstr "" msgstr ""
#: models.py:78 #: models.py:80
msgid "Executed" msgid "Executed"
msgstr "" msgstr ""
#: models.py:91 #: models.py:94
msgid "Amendment Fee" msgid "Amendment Fee"
msgstr "" msgstr ""
#: models.py:92 #: models.py:95
#, fuzzy
#| msgid "Invoice"
msgid "Abono Invoice"
msgstr "Abono"
#: models.py:96
msgid "Pro forma" msgid "Pro forma"
msgstr "" msgstr ""
#: models.py:99 #: models.py:103
msgid "number" msgid "number"
msgstr "número" msgstr "número"
#: models.py:102 #: models.py:106
msgid "amend of" msgid "amend of"
msgstr "rectificación de" msgstr "rectificación de"
#: models.py:105 #: models.py:109
msgid "created on" msgid "created on"
msgstr "creado en" msgstr "creado en"
#: models.py:106 #: models.py:110
msgid "closed on" msgid "closed on"
msgstr "cerrada en" msgstr "cerrada en"
#: models.py:107 #: models.py:111
msgid "open" msgid "open"
msgstr "abierta" msgstr "abierta"
#: models.py:108 #: models.py:112
msgid "sent" msgid "sent"
msgstr "enviada" msgstr "enviada"
#: models.py:109 #: models.py:113
msgid "due on" msgid "due on"
msgstr "vencimiento" msgstr "vencimiento"
#: models.py:110 #: models.py:114
msgid "updated on" msgid "updated on"
msgstr "actualizada en" msgstr "actualizada en"
#: models.py:112 #: models.py:116
msgid "comments" msgid "comments"
msgstr "comentarios" msgstr "comentarios"
#: models.py:113 #: models.py:117
msgid "HTML" msgid "HTML"
msgstr "HTML" msgstr "HTML"
#: models.py:194 #: models.py:200
#, python-format #, python-format
msgid "Type %s is not an amendment." msgid "Type %s is not an amendment."
msgstr "" msgstr ""
#: models.py:196 #: models.py:202
msgid "Amend of related account doesn't match bill account." msgid "Amend of related account doesn't match bill account."
msgstr "" msgstr ""
#: models.py:198 #: models.py:204
#, fuzzy #, fuzzy
#| msgid "Selected bills should be in open state" #| msgid "Selected bills should be in open state"
msgid "Related invoice is in open state." msgid "Related invoice is in open state."
msgstr "Las facturas seleccionadas están en estado abierto" msgstr "Las facturas seleccionadas están en estado abierto"
#: models.py:200 #: models.py:206
msgid "Related invoice is an amendment." msgid "Related invoice is an amendment."
msgstr "" msgstr ""
#: models.py:392 #: models.py:419
msgid "bill" msgid "bill"
msgstr "factura" msgstr "factura"
#: models.py:393 models.py:458 templates/bills/microspective.html:73 #: models.py:420 models.py:499 templates/bills/microspective.html:75
msgid "description" msgid "description"
msgstr "descripción" msgstr "descripción"
#: models.py:394 #: models.py:421
msgid "rate" msgid "rate"
msgstr "tarifa" msgstr "tarifa"
#: models.py:395 #: models.py:422
msgid "quantity" msgid "quantity"
msgstr "cantidad" msgstr "cantidad"
#: models.py:397 #: models.py:424
msgid "Verbose quantity" msgid "Verbose quantity"
msgstr "Cantidad" msgstr "Cantidad"
#: models.py:398 templates/admin/bills/bill/report.html:47 #: models.py:425 templates/admin/bills/bill/report.html:47
#: templates/bills/microspective.html:77 #: templates/bills/microspective.html:79
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
msgid "subtotal" msgid "subtotal"
msgstr "subtotal" msgstr "subtotal"
#: models.py:399 #: models.py:426
msgid "tax" msgid "tax"
msgstr "impuesto" msgstr "impuesto"
#: models.py:400 #: models.py:427
msgid "start" msgid "start"
msgstr "inicio" msgstr "inicio"
#: models.py:401 #: models.py:428
msgid "end" msgid "end"
msgstr "fín" msgstr "fín"
#: models.py:403 #: models.py:431
msgid "Informative link back to the order" msgid "Informative link back to the order"
msgstr "" msgstr ""
#: models.py:404 #: models.py:432
msgid "order billed" msgid "order billed"
msgstr "" msgstr ""
#: models.py:405 #: models.py:433
msgid "order billed until" msgid "order billed until"
msgstr "" msgstr ""
#: models.py:406 #: models.py:434
msgid "created" msgid "created"
msgstr "creado" msgstr "creado"
#: models.py:408 #: models.py:436
msgid "amended line" msgid "amended line"
msgstr "linea rectificativa" msgstr "linea rectificativa"
#: models.py:451 #: models.py:492
msgid "Volume" msgid "Volume"
msgstr "Volumen" msgstr "Volumen"
#: models.py:452 #: models.py:493
msgid "Compensation" msgid "Compensation"
msgstr "Compensación" msgstr "Compensación"
#: models.py:453 #: models.py:494
msgid "Other" msgid "Other"
msgstr "Otro" msgstr "Otro"
#: models.py:457 #: models.py:498
msgid "bill line" msgid "bill line"
msgstr "linea de factura" msgstr "linea de factura"
#: templates/admin/bills/bill/change_list.html:9
msgid "Lines"
msgstr ""
#: templates/admin/bills/bill/change_list.html:15
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/admin/bills/bill/close_send_download_bills.html:57
msgid "Yes, I'm sure"
msgstr ""
#: templates/admin/bills/bill/report.html:42 #: templates/admin/bills/bill/report.html:42
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
@ -523,19 +570,19 @@ msgstr ""
#: templates/admin/bills/bill/report.html:47 #: templates/admin/bills/bill/report.html:47
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/admin/bills/bill/report.html:69 #: templates/admin/bills/bill/report.html:69
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:116
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "VAT" msgid "VAT"
msgstr "IVA" msgstr "IVA"
#: templates/admin/bills/bill/report.html:51 #: templates/admin/bills/bill/report.html:51
#: templates/bills/microspective.html:114 #: templates/bills/microspective.html:119
msgid "taxes" msgid "taxes"
msgstr "impuestos" msgstr "impuestos"
#: templates/admin/bills/bill/report.html:56 #: templates/admin/bills/bill/report.html:56
#: templates/admin/bills/billline/report.html:60 #: templates/admin/bills/billline/report.html:60
#: templates/bills/microspective.html:53 #: templates/bills/microspective.html:54
msgid "TOTAL" msgid "TOTAL"
msgstr "TOTAL" msgstr "TOTAL"
@ -553,8 +600,20 @@ msgstr "Fecha de pago"
msgid "Base" msgid "Base"
msgstr "Base" msgstr "Base"
#: templates/admin/bills/billline/change_list.html:6
msgid "Home"
msgstr ""
#: templates/admin/bills/billline/change_list.html:8
msgid "Bills"
msgstr ""
#: templates/admin/bills/billline/change_list.html:9
msgid "Multiple bills"
msgstr ""
#: templates/admin/bills/billline/report.html:42 #: templates/admin/bills/billline/report.html:42
msgid "Services" msgid "Service"
msgstr "" msgstr ""
#: templates/admin/bills/billline/report.html:43 #: templates/admin/bills/billline/report.html:43
@ -579,62 +638,56 @@ msgstr "cantidad"
msgid "Profit" msgid "Profit"
msgstr "" msgstr ""
#: templates/admin/bills/change_list.html:9 #: templates/bills/microspective-fee.html:115
#, fuzzy
#| msgid "bill"
msgid "Add bill"
msgstr "factura"
#: templates/bills/microspective-fee.html:107
msgid "Due date" msgid "Due date"
msgstr "Fecha de pago" msgstr "Fecha de pago"
#: templates/bills/microspective-fee.html:108 #: templates/bills/microspective-fee.html:116
#, python-format #, python-format
msgid "On %(bank_account)s" msgid "On %(bank_account)s"
msgstr "En %(bank_account)s" msgstr "En %(bank_account)s"
#: templates/bills/microspective-fee.html:114 #: templates/bills/microspective-fee.html:122
#, python-format #, python-format
msgid "From %(ini)s to %(end)s" msgid "From %(ini)s to %(end)s"
msgstr "Desde %(ini)s hasta %(end)s" msgstr "Desde %(ini)s hasta %(end)s"
#: templates/bills/microspective-fee.html:121 #: templates/bills/microspective-fee.html:144
msgid "" msgid ""
"\n" "\n"
"<strong>With your membership</strong> you are supporting ...\n" "<strong>With your membership</strong> you are supporting ...\n"
msgstr "" msgstr ""
#: templates/bills/microspective.html:49 #: templates/bills/microspective.html:50
msgid "DUE DATE" msgid "DUE DATE"
msgstr "VENCIMIENTO" msgstr "VENCIMIENTO"
#: templates/bills/microspective.html:57 #: templates/bills/microspective.html:58
#, python-format #, python-format
msgid "%(bill_type)s DATE" msgid "%(bill_type)s DATE"
msgstr "FECHA %(bill_type)s" msgstr "FECHA %(bill_type)s"
#: templates/bills/microspective.html:74 #: templates/bills/microspective.html:76
msgid "period" msgid "period"
msgstr "periodo" msgstr "periodo"
#: templates/bills/microspective.html:75 #: templates/bills/microspective.html:77
msgid "hrs/qty" msgid "hrs/qty"
msgstr "hrs/cant" msgstr "hrs/cant"
#: templates/bills/microspective.html:76 #: templates/bills/microspective.html:78
msgid "rate/price" msgid "rate/price"
msgstr "tarifa/precio" msgstr "tarifa/precio"
#: templates/bills/microspective.html:131 #: templates/bills/microspective.html:137
msgid "COMMENTS" msgid "COMMENTS"
msgstr "COMENTARIOS" msgstr "COMENTARIOS"
#: templates/bills/microspective.html:138 #: templates/bills/microspective.html:145
msgid "PAYMENT" msgid "PAYMENT"
msgstr "PAGO" msgstr "PAGO"
#: templates/bills/microspective.html:142 #: templates/bills/microspective.html:149
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -648,11 +701,11 @@ msgstr ""
"Puedes pagar esta <i>%(type)s</i> por transferencia bancaria.<br>Incluye tu " "Puedes pagar esta <i>%(type)s</i> por transferencia bancaria.<br>Incluye tu "
"nombre y el número de <i>%(type)s</i>. Nuestra cuenta bancaria es" "nombre y el número de <i>%(type)s</i>. Nuestra cuenta bancaria es"
#: templates/bills/microspective.html:151 #: templates/bills/microspective.html:160
msgid "QUESTIONS" msgid "QUESTIONS"
msgstr "PREGUNTAS" msgstr "PREGUNTAS"
#: templates/bills/microspective.html:152 #: templates/bills/microspective.html:161
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -668,3 +721,8 @@ msgstr ""
" contacta con nosotros en %(email)s. Te responderemos lo más " " contacta con nosotros en %(email)s. Te responderemos lo más "
"rapidamente posible.\n" "rapidamente posible.\n"
" " " "
#, fuzzy
#~| msgid "closed on"
#~ msgid "No closed amends"
#~ msgstr "cerrada en"

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

File diff suppressed because one or more lines are too long

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

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

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

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

View file

@ -18,6 +18,9 @@ BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX = Setting('BILLS_AMENDMENT_INVOICE_NUMBER_
'A' 'A'
) )
BILLS_ABONOINVOICE_NUMBER_PREFIX = Setting('BILLS_ABONOINVOICE_NUMBER_PREFIX',
'AB'
)
BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX', BILLS_FEE_NUMBER_PREFIX = Setting('BILLS_FEE_NUMBER_PREFIX',
'F' 'F'

View file

@ -282,3 +282,17 @@ a:hover {
#questions { #questions {
margin-bottom: 0px; margin-bottom: 0px;
} }
#watermark {
color: #d0d0d0;
font-size: 100pt;
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
position: absolute;
width: 100%;
height: 100%;
margin: 0;
z-index: -1;
max-width: 593px;
}

View file

@ -12,6 +12,12 @@
{% block body %} {% block body %}
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
{% if bill.is_open %}
<!-- TODO DANIEL: falta arreglar el css d'aquesta cosa -->
<div id="watermark">
<p>ESBORRANY - DRAFT - BORRADOR</p>
</div>
{% endif %}
{% block header %} {% block header %}
<div id="logo"> <div id="logo">
{% block logo %} {% block logo %}

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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() objects = ContactQuerySet.as_manager()
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='contacts', null=True) related_name='contacts', null=True, on_delete=models.SET_NULL)
short_name = models.CharField(_("short name"), max_length=128) short_name = models.CharField(_("short name"), max_length=128)
full_name = models.CharField(_("full name"), max_length=256, blank=True) full_name = models.CharField(_("full name"), max_length=256, blank=True)
email = models.EmailField() email = models.EmailField()

View file

@ -1,6 +1,8 @@
from django.conf.urls import url from django.conf.urls import url
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
@ -12,6 +14,10 @@ from .filters import HasUserListFilter, HasDatabaseListFilter
from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm
from .models import Database, DatabaseUser from .models import Database, DatabaseUser
def save_selected(modeladmin, request, queryset):
for selected in queryset:
selected.save()
save_selected.short_description = "Re-save selected objects"
class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'type', 'display_users', 'account_link') list_display = ('name', 'type', 'display_users', 'account_link')
@ -22,7 +28,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'classes': ('extrapretty',), 'classes': ('extrapretty',),
'fields': ('account_link', 'name', 'type', 'users', 'display_users'), 'fields': ('account_link', 'name', 'type', 'users', 'display_users', 'comments'),
}), }),
) )
add_fieldsets = ( add_fieldsets = (
@ -44,16 +50,16 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
filter_horizontal = ['users'] filter_horizontal = ['users']
filter_by_account_fields = ('users',) filter_by_account_fields = ('users',)
list_prefetch_related = ('users',) list_prefetch_related = ('users',)
actions = (list_accounts,) actions = (list_accounts, save_selected)
@mark_safe
def display_users(self, db): def display_users(self, db):
links = [] links = []
for user in db.users.all(): for user in db.users.all():
link = '<a href="%s">%s</a>' % (change_url(user), user.username) link = format_html('<a href="{}">{}</a>', change_url(user), user.username)
links.append(link) links.append(link)
return '<br>'.join(links) return '<br>'.join(links)
display_users.short_description = _("Users") display_users.short_description = _("Users")
display_users.allow_tags = True
display_users.admin_order_field = 'users__username' display_users.admin_order_field = 'users__username'
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
@ -93,16 +99,16 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
readonly_fields = ('account_link', 'display_databases',) readonly_fields = ('account_link', 'display_databases',)
filter_by_account_fields = ('databases',) filter_by_account_fields = ('databases',)
list_prefetch_related = ('databases',) list_prefetch_related = ('databases',)
actions = (list_accounts,) actions = (list_accounts, save_selected)
@mark_safe
def display_databases(self, user): def display_databases(self, user):
links = [] links = []
for db in user.databases.all(): for db in user.databases.all():
link = '<a href="%s">%s</a>' % (change_url(db), db.name) link = format_html('<a href="{}">{}</a>', change_url(db), db.name)
links.append(link) links.append(link)
return '<br>'.join(links) return '<br>'.join(links)
display_databases.short_description = _("Databases") display_databases.short_description = _("Databases")
display_databases.allow_tags = True
display_databases.admin_order_field = 'databases__name' display_databases.admin_order_field = 'databases__name'
def get_urls(self): def get_urls(self):

View file

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

View file

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

View file

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:25
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.core.validators
class Migration(migrations.Migration):
replaces = [('databases', '0001_initial'), ('databases', '0002_auto_20170528_2005'), ('databases', '0003_database_comments'), ('databases', '0004_auto_20210330_1049')]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Database',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
],
),
migrations.CreateModel(
name='DatabaseUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=16, validators=[orchestra.core.validators.validate_name], verbose_name='username')),
('password', models.CharField(max_length=256, verbose_name='password')),
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
],
options={
'verbose_name_plural': 'DB users',
},
),
migrations.AddField(
model_name='database',
name='users',
field=models.ManyToManyField(blank=True, related_name='databases', to='databases.DatabaseUser', verbose_name='users'),
),
migrations.AlterUniqueTogether(
name='databaseuser',
unique_together=set([('username', 'type')]),
),
migrations.AlterUniqueTogether(
name='database',
unique_together=set([('name', 'type')]),
),
migrations.AlterField(
model_name='database',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='databaseuser',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AddField(
model_name='database',
name='comments',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='database',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='databaseuser',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('databases', '0001_initial'),
]
operations = [
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'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2020-02-04 11:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('databases', '0002_auto_20170528_2005'),
]
operations = [
migrations.AddField(
model_name='database',
name='comments',
field=models.TextField(default=''),
),
]

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-03-30 10:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('databases', '0003_database_comments'),
]
operations = [
migrations.AlterField(
model_name='database',
name='comments',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='database',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='databaseuser',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
),
]

View file

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

View file

@ -20,7 +20,7 @@ DATABASES_DEFAULT_TYPE = Setting('DATABASES_DEFAULT_TYPE',
DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST', DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST',
'localhost', 'localhost',
validators=[validate_hostname], # validators=[validate_hostname],
) )

View file

@ -1,22 +1,24 @@
import MySQLdb
import os import os
import socket import socket
import time import time
import unittest
import MySQLdb
from django.conf import settings as djsettings from django.conf import settings as djsettings
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.core.urlresolvers import reverse from django.urls import reverse
from selenium.webdriver.support.select import Select
from orchestra.admin.utils import change_url from orchestra.admin.utils import change_url
from orchestra.contrib.orchestration.models import Server, Route from orchestra.contrib.orchestration.models import Route, Server
from orchestra.utils.sys import sshrun from orchestra.utils.sys import sshrun
from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error, from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii,
snapshot_on_error) save_response_on_error, snapshot_on_error)
from selenium.webdriver.support.select import Select
from ... import backends, settings from ... import backends, settings
from ...models import Database, DatabaseUser from ...models import Database, DatabaseUser
TEST_REST_API = int(os.getenv('TEST_REST_API', '0'))
class DatabaseTestMixin(object): class DatabaseTestMixin(object):
MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost') MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost')
@ -181,6 +183,7 @@ class MySQLControllerMixin(object):
"""mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout) """mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout)
@unittest.skipUnless(TEST_REST_API, "REST API tests")
class RESTDatabaseMixin(DatabaseTestMixin): class RESTDatabaseMixin(DatabaseTestMixin):
def setUp(self): def setUp(self):
super(RESTDatabaseMixin, self).setUp() super(RESTDatabaseMixin, self).setUp()

View file

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

View file

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

View file

@ -102,7 +102,7 @@ class Bind9MasterDomainController(ServiceController):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
# Apply changes # Apply changes
if [[ $UPDATED == 1 ]]; then if [[ $UPDATED == 1 ]]; then
service bind9 reload rm /etc/bind/master/*jnl || true; service bind9 restart
fi""") fi""")
) )
@ -158,6 +158,7 @@ class Bind9MasterDomainController(ServiceController):
'slaves': '; '.join(slaves) or 'none', 'slaves': '; '.join(slaves) or 'none',
'also_notify': '; '.join(slaves) + ';' if slaves else '', 'also_notify': '; '.join(slaves) + ';' if slaves else '',
'conf_path': self.CONF_PATH, 'conf_path': self.CONF_PATH,
'dns2136_address_match_list': domain.dns2136_address_match_list
} }
context['conf'] = textwrap.dedent("""\ context['conf'] = textwrap.dedent("""\
zone "%(name)s" { zone "%(name)s" {
@ -166,6 +167,7 @@ class Bind9MasterDomainController(ServiceController):
file "%(zone_path)s"; file "%(zone_path)s";
allow-transfer { %(slaves)s; }; allow-transfer { %(slaves)s; };
also-notify { %(also_notify)s }; also-notify { %(also_notify)s };
allow-update { %(dns2136_address_match_list)s };
notify yes; notify yes;
};""") % context };""") % context
return context return context

View file

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

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:27
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.contrib.domains.utils
import orchestra.contrib.domains.validators
class Migration(migrations.Migration):
replaces = [('domains', '0001_initial'), ('domains', '0002_auto_20150715_1017'), ('domains', '0003_auto_20150720_1121'), ('domains', '0004_auto_20150720_1121'), ('domains', '0005_auto_20160219_1034'), ('domains', '0006_auto_20170528_2011'), ('domains', '0007_auto_20190805_1134'), ('domains', '0008_domain_dns2136_address_match_list'), ('domains', '0009_auto_20200204_1217'), ('domains', '0010_auto_20210330_1049')]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Domain',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name')),
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')),
('account', models.ForeignKey(blank=True, help_text='Automatically selected for subdomains.', on_delete=django.db.models.deletion.CASCADE, related_name='domains', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('top', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain')),
],
),
migrations.CreateModel(
name='Record',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ttl', models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL')),
('type', models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF')], max_length=32, verbose_name='type')),
('value', models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
],
),
migrations.AlterField(
model_name='domain',
name='serial',
field=models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, editable=False, help_text='A revision number that changes whenever this domain is updated.', verbose_name='serial'),
),
migrations.AddField(
model_name='domain',
name='expire',
field=models.CharField(blank=True, help_text='The time that a secondary server will keep trying to complete a zone transfer. If this time expires prior to a successful zone transfer, the secondary server will expire its zone file. This means the secondary will stop answering queries. The default value is <tt>4w</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='expire'),
),
migrations.AddField(
model_name='domain',
name='min_ttl',
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
),
migrations.AddField(
model_name='domain',
name='refresh',
field=models.CharField(blank=True, help_text="The time a secondary DNS server waits before querying the primary DNS server's SOA record to check for changes. When the refresh time expires, the secondary DNS server requests a copy of the current SOA record from the primary. The primary DNS server complies with this request. The secondary DNS server compares the serial number of the primary DNS server's current SOA record and the serial number in it's own SOA record. If they are different, the secondary DNS server will request a zone transfer from the primary DNS server. The default value is <tt>1d</tt>.", max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='refresh'),
),
migrations.AddField(
model_name='domain',
name='retry',
field=models.CharField(blank=True, help_text='The time a secondary server waits before retrying a failed zone transfer. Normally, the retry time is less than the refresh time. The default value is <tt>2h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='retry'),
),
migrations.AlterField(
model_name='domain',
name='name',
field=models.CharField(db_index=True, help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name'),
),
migrations.AlterField(
model_name='domain',
name='top',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain', verbose_name='top domain'),
),
migrations.AddField(
model_name='domain',
name='dns2136_address_match_list',
field=models.CharField(blank=True, default='key pangea.key;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
),
]

View file

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

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.db import migrations, models
import orchestra.contrib.domains.validators
class Migration(migrations.Migration):
dependencies = [
('domains', '0005_auto_20160219_1034'),
]
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>30m</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 30m', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
),
migrations.AlterField(
model_name='record',
name='type',
field=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'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2019-08-05 09:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('domains', '0006_auto_20170528_2011'),
]
operations = [
migrations.AlterField(
model_name='record',
name='value',
field=models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2019-09-20 07:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('domains', '0007_auto_20190805_1134'),
]
operations = [
migrations.AddField(
model_name='domain',
name='dns2136_address_match_list',
field=models.CharField(blank=True, default='none;', 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

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2020-02-04 11:17
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('domains', '0008_domain_dns2136_address_match_list'),
]
operations = [
migrations.AlterField(
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

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-03-30 10:49
from __future__ import unicode_literals
from django.db import migrations, models
import orchestra.contrib.domains.validators
class Migration(migrations.Migration):
dependencies = [
('domains', '0009_auto_20200204_1217'),
]
operations = [
migrations.AlterField(
model_name='domain',
name='min_ttl',
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
),
migrations.AlterField(
model_name='record',
name='ttl',
field=models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
),
]

View file

@ -31,9 +31,9 @@ class Domain(models.Model):
validators.validate_allowed_domain validators.validate_allowed_domain
]) ])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True, account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
related_name='domains', help_text=_("Automatically selected for subdomains.")) related_name='domains', on_delete=models.CASCADE, help_text=_("Automatically selected for subdomains."))
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set', top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
editable=False, verbose_name=_("top domain")) editable=False, verbose_name=_("top domain"), on_delete=models.CASCADE)
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False, serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
help_text=_("A revision number that changes whenever this domain is updated.")) help_text=_("A revision number that changes whenever this domain is updated."))
refresh = models.CharField(_("refresh"), max_length=16, blank=True, refresh = models.CharField(_("refresh"), max_length=16, blank=True,
@ -65,6 +65,10 @@ class Domain(models.Model):
"zone file. This value is supplied in query responses to inform other " "zone file. This value is supplied in query responses to inform other "
"servers how long they should keep the data in cache. " "servers how long they should keep the data in cache. "
"The default value is <tt>%s</tt>.") % settings.DOMAINS_DEFAULT_MIN_TTL) "The default value is <tt>%s</tt>.") % settings.DOMAINS_DEFAULT_MIN_TTL)
dns2136_address_match_list = models.CharField(max_length=80, default=settings.DOMAINS_DEFAULT_DNS2136,
blank=True,
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.")
objects = DomainQuerySet.as_manager() objects = DomainQuerySet.as_manager()
@ -314,12 +318,13 @@ class Record(models.Model):
SOA: (validators.validate_soa_record,), SOA: (validators.validate_soa_record,),
} }
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records') domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE)
ttl = models.CharField(_("TTL"), max_length=8, blank=True, ttl = models.CharField(_("TTL"), max_length=8, blank=True,
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL, help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
validators=[validators.validate_zone_interval]) validators=[validators.validate_zone_interval])
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES) type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
value = models.CharField(_("value"), max_length=256, # max_length bumped from 256 to 1024 (arbitrary) on August 2019.
value = models.CharField(_("value"), max_length=1024,
help_text=_("MX, NS and CNAME records sould end with a dot.")) help_text=_("MX, NS and CNAME records sould end with a dot."))
def __str__(self): def __str__(self):

View file

@ -122,3 +122,6 @@ DOMAINS_MASTERS = Setting('DOMAINS_MASTERS',
validators=[lambda masters: list(map(validate_ip_address, masters))], validators=[lambda masters: list(map(validate_ip_address, masters))],
help_text="Additional master server ip addresses other than autodiscovered by router.get_servers()." help_text="Additional master server ip addresses other than autodiscovered by router.get_servers()."
) )
#TODO remove pangea-specific default
DOMAINS_DEFAULT_DNS2136 = "key pangea.key;"

View file

@ -4,7 +4,7 @@ import socket
from functools import partial from functools import partial
from django.conf import settings as djsettings from django.conf import settings as djsettings
from django.core.urlresolvers import reverse from django.urls import reverse
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
from orchestra.contrib.orchestration.models import Server, Route from orchestra.contrib.orchestration.models import Server, Route

View file

@ -60,7 +60,7 @@ def validate_zone_label(value):
if not value.endswith('.'): if not value.endswith('.'):
msg = _("Use a fully expanded domain name ending with a dot.") msg = _("Use a fully expanded domain name ending with a dot.")
raise ValidationError(msg) raise ValidationError(msg)
if len(value) > 63: if len(value) > 254:
raise ValidationError(_("Labels must be 63 characters or less.")) raise ValidationError(_("Labels must be 63 characters or less."))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:27
from __future__ import unicode_literals
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.utils.timezone import utc
import orchestra.models.fields
class Migration(migrations.Migration):
replaces = [('issues', '0001_initial'), ('issues', '0002_auto_20150709_1018'), ('issues', '0003_auto_20160320_1127'), ('issues', '0004_auto_20170528_2011')]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
('content', models.TextField(verbose_name='content')),
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
],
options={
'get_latest_by': 'id',
},
),
migrations.CreateModel(
name='Queue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True, verbose_name='name')),
('verbose_name', models.CharField(blank=True, max_length=128, verbose_name='verbose_name')),
('default', models.BooleanField(default=False, verbose_name='default')),
('notify', orchestra.models.fields.MultiSelectField(blank=True, choices=[('SUPPORT', 'Support tickets'), ('ADMIN', 'Administrative'), ('BILLING', 'Billing'), ('TECH', 'Technical'), ('ADDS', 'Announcements'), ('EMERGENCY', 'Emergency contact')], default=('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY'), help_text='Contacts to notify by email', max_length=256, verbose_name='notify')),
],
),
migrations.CreateModel(
name='Ticket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creator_name', models.CharField(blank=True, max_length=256, verbose_name='creator name')),
('subject', models.CharField(max_length=256, verbose_name='subject')),
('description', models.TextField(verbose_name='description')),
('priority', models.CharField(choices=[('HIGH', 'High'), ('MEDIUM', 'Medium'), ('LOW', 'Low')], default='MEDIUM', max_length=32, verbose_name='priority')),
('state', models.CharField(choices=[('NEW', 'New'), ('IN_PROGRESS', 'In Progress'), ('RESOLVED', 'Resolved'), ('FEEDBACK', 'Feedback'), ('REJECTED', 'Rejected'), ('CLOSED', 'Closed')], default='NEW', max_length=32, verbose_name='state')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
('queue', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='issues.Queue')),
],
options={
'ordering': ['-updated_at'],
},
),
migrations.CreateModel(
name='TicketTracker',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
),
migrations.AddField(
model_name='message',
name='ticket',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
),
migrations.AlterUniqueTogether(
name='tickettracker',
unique_together=set([('ticket', 'user')]),
),
migrations.AlterField(
model_name='ticket',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created'),
),
migrations.RemoveField(
model_name='message',
name='created_on',
),
migrations.AddField(
model_name='message',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 20, 10, 27, 45, 766388, tzinfo=utc), verbose_name='created at'),
preserve_default=False,
),
migrations.AlterField(
model_name='ticket',
name='creator',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by'),
),
migrations.AlterField(
model_name='ticket',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to'),
),
migrations.AlterField(
model_name='ticket',
name='queue',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='issues.Queue'),
),
]

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-28 18:11
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('issues', '0003_auto_20160320_1127'),
]
operations = [
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): class Message(models.Model):
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"), ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
related_name='messages') verbose_name=_("ticket"), related_name='messages')
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"), author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
related_name='ticket_messages') verbose_name=_("author"), related_name='ticket_messages')
author_name = models.CharField(_("author name"), max_length=256, blank=True) author_name = models.CharField(_("author name"), max_length=256, blank=True)
content = models.TextField(_("content")) content = models.TextField(_("content"))
created_at = models.DateTimeField(_("created at"), auto_now_add=True) created_at = models.DateTimeField(_("created at"), auto_now_add=True)
@ -191,9 +191,10 @@ class Message(models.Model):
class TicketTracker(models.Model): class TicketTracker(models.Model):
""" Keeps track of user read tickets """ """ Keeps track of user read tickets """
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers') ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"), verbose_name=_("ticket"), related_name='trackers')
related_name='ticket_trackers') user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
verbose_name=_("user"), related_name='ticket_trackers')
class Meta: class Meta:
unique_together = ( unique_together = (

View file

@ -10,7 +10,7 @@ from .serializers import ListSerializer
class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet): class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
queryset = List.objects.all() queryset = List.objects.all()
serializer_class = ListSerializer serializer_class = ListSerializer
filter_fields = ('name',) filter_fields = ('name', 'address_domain')
router.register(r'lists', ListViewSet) router.register(r'lists', ListViewSet)

View file

@ -48,20 +48,14 @@ class MailmanVirtualDomainController(ServiceController):
def save(self, mail_list): def save(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.include_virtual_alias_domain(context) #self.include_virtual_alias_domain(context)
def delete(self, mail_list): def delete(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.exclude_virtual_alias_domain(context) #self.exclude_virtual_alias_domain(context)
def commit(self): def commit(self):
context = self.get_context_files() context = self.get_context_files()
self.append(textwrap.dedent("""
# Apply changes if needed
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
service postfix reload
fi""") % context
)
super(MailmanVirtualDomainController, self).commit() super(MailmanVirtualDomainController, self).commit()
def get_context_files(self): def get_context_files(self):
@ -107,7 +101,7 @@ class MailmanController(MailmanVirtualDomainController):
for suffix in self.address_suffixes: for suffix in self.address_suffixes:
context['suffix'] = suffix context['suffix'] = suffix
# Because mailman doesn't properly handle lists aliases we need two virtual aliases # Because mailman doesn't properly handle lists aliases we need two virtual aliases
aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context) aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s@grups.pangea.org" % context)
if context['address_name'] != context['name']: if context['address_name'] != context['name']:
# And another with the original list name; Mailman generates links with it # And another with the original list name; Mailman generates links with it
aliases.append("%(name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context) aliases.append("%(name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context)
@ -116,84 +110,21 @@ class MailmanController(MailmanVirtualDomainController):
def save(self, mail_list): def save(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
# Create list # Create list
self.append(textwrap.dedent(""" cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/save.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
# Create list %(name)s if not mail_list.active:
[[ ! -e '%(mailman_root)s/lists/%(name)s' ]] && { cmd += ' --inactive'
newlist --quiet --emailhost='%(domain)s' '%(name)s' '%(admin)s' '%(password)s' self.append(cmd)
}""") % context)
# Custom domain
if mail_list.address:
context.update({
'aliases': self.get_virtual_aliases(context),
'num_entries': 2 if context['address_name'] != context['name'] else 1,
})
self.append(textwrap.dedent("""\
# Create list alias for custom domain
aliases='%(aliases)s'
if ! grep '\s\s*%(name)s\s*$' %(virtual_alias)s > /dev/null; then
echo "${aliases}" >> %(virtual_alias)s
UPDATED_VIRTUAL_ALIAS=1
else
existing=$({ grep -E '^\s*(%(address_name)s|%(name)s)@%(address_domain)s\s\s*%(name)s\s*$' %(virtual_alias)s || test $? -lt 2; }|wc -l)
if [[ $existing -ne %(num_entries)s ]]; then
sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s
echo "${aliases}" >> %(virtual_alias)s
UPDATED_VIRTUAL_ALIAS=1
fi
fi
echo "require_explicit_destination = 0" | \\
%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s
echo "host_name = '%(address_domain)s'" | \\
%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s""") % context
)
else:
self.append(textwrap.dedent("""\
# Cleanup possible ex-custom domain
if ! grep '\s\s*%(name)s\s*$' %(virtual_alias)s > /dev/null; then
sed -i "/^.*\s%(name)s\s*$/d" %(virtual_alias)s
fi""") % context
)
# Update
if context['password'] is not None:
self.append(textwrap.dedent("""\
# Re-set password
%(mailman_root)s/bin/change_pw --listname="%(name)s" --password="%(password)s"\
""") % context
)
self.include_virtual_alias_domain(context)
if mail_list.active:
self.append('chmod 775 %(mailman_root)s/lists/%(name)s' % context)
else:
self.append('chmod 000 %(mailman_root)s/lists/%(name)s' % context)
def delete(self, mail_list): def delete(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.exclude_virtual_alias_domain(context) # Delete list
self.append(textwrap.dedent(""" cmd = "/opt/mailman/venv/bin/python /usr/local/admin/orchestra_mailman3/delete.py %(name)s %(admin)s %(address_name)s@%(domain)s" % context
# Remove list %(name)s if not mail_list.active:
sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\ cmd += ' --inactive'
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s self.append(cmd)
# Non-existent list archives produce exit code 1
exit_code=0
rmlist -a %(name)s || exit_code=$?
if [[ $exit_code != 0 && $exit_code != 1 ]]; then
exit $exit_code
fi""") % context
)
def commit(self): def commit(self):
context = self.get_context_files() pass
self.append(textwrap.dedent("""
# Apply changes if needed
if [[ $UPDATED_VIRTUAL_ALIAS == 1 ]]; then
postmap %(virtual_alias)s
fi
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
service postfix reload
fi
exit $exit_code""") % context
)
def get_context_files(self): def get_context_files(self):
return { return {

View file

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

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