From e1224ddd57f808d3165265ecd55d361d25823407 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 8 Jun 2021 10:02:59 +0200 Subject: [PATCH 01/12] Add django_filters to INSTALLED_APPS Fix TemplateDoesNotExist django_filters/rest_framework/form.html --- orchestra/conf/project_template/project_name/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/orchestra/conf/project_template/project_name/settings.py b/orchestra/conf/project_template/project_name/settings.py index 576dd0b8..f8c8d6a2 100644 --- a/orchestra/conf/project_template/project_name/settings.py +++ b/orchestra/conf/project_template/project_name/settings.py @@ -66,6 +66,7 @@ INSTALLED_APPS = [ 'admin_tools.dashboard', 'rest_framework', 'rest_framework.authtoken', + 'django_filters', 'passlib.ext.django', 'django_countries', # 'debug_toolbar', From 5e7a8232056904bc5ca20ddfc8bd1c941385a745 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 8 Jun 2021 10:05:40 +0200 Subject: [PATCH 02/12] Revert "documentation ribaguifi style instalation" This reverts commit 5b4b7310e659853d5eba36a840c8ff46932455cc. Remove duplicated project settings template. --- .env.example | 5 - INSTALL_RIBAGUIFI_STYLE.md | 70 ----- install_manually.md | 2 +- .../conf/ribaguifi_template/locale/.gitignore | 0 orchestra/conf/ribaguifi_template/manage.py | 13 - .../conf/ribaguifi_template/media/.gitignore | 0 .../project_name/__init__.py | 0 .../project_name/settings.py | 257 ------------------ .../ribaguifi_template/project_name/urls.py | 6 - .../ribaguifi_template/project_name/wsgi.py | 14 - total_requirements.txt | 39 --- 11 files changed, 1 insertion(+), 405 deletions(-) delete mode 100644 .env.example delete mode 100644 INSTALL_RIBAGUIFI_STYLE.md delete mode 100644 orchestra/conf/ribaguifi_template/locale/.gitignore delete mode 100755 orchestra/conf/ribaguifi_template/manage.py delete mode 100644 orchestra/conf/ribaguifi_template/media/.gitignore delete mode 100644 orchestra/conf/ribaguifi_template/project_name/__init__.py delete mode 100644 orchestra/conf/ribaguifi_template/project_name/settings.py delete mode 100644 orchestra/conf/ribaguifi_template/project_name/urls.py delete mode 100644 orchestra/conf/ribaguifi_template/project_name/wsgi.py delete mode 100644 total_requirements.txt diff --git a/.env.example b/.env.example deleted file mode 100644 index 15bbe394..00000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -SECRET_KEY=k_=*vfue(^campsl63)7w5m&cu9u4o4-!vaw94qzyrymyv0hgg -DEBUG=True -ALLOWED_HOSTS=.localhost,127.0.0.1 -DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME -STATIC_ROOT=PATH_TO_STATIC_ROOT diff --git a/INSTALL_RIBAGUIFI_STYLE.md b/INSTALL_RIBAGUIFI_STYLE.md deleted file mode 100644 index ee821040..00000000 --- a/INSTALL_RIBAGUIFI_STYLE.md +++ /dev/null @@ -1,70 +0,0 @@ -We need have python3.6 - -#Install Packages -```bash -apt=( - bind9utils - ca-certificates - gettext - libcrack2-dev - libxml2-dev - libxslt1-dev - ssh-client - wget - xvfb - zlib1g-dev - git - iceweasel - dnsutils - postgresql-contrib -) -sudo apt-get install --no-install-recommends -y ${apt[@]} -``` - -It is necessary install *wkhtmltopdf* -You can install it from https://wkhtmltopdf.org/downloads.html - -Clone this repository -```bash -git clone https://github.com/ribaguifi/django-orchestra -``` - -Prepare env and install requirements -```bash -cd django-orchestra -python3.6 -m venv env -source env/bin/activate -pip3 install --upgrade pip -pip3 install -r total_requirements.txt -pip3 install -e . -``` - -Configure project using environment file (you can use provided example as quickstart): -```bash -cp .env.example .env -``` - -Prepare your Postgres database (create database, user and grant permissions): -```sql -CREATE DATABASE myproject; -CREATE USER myuser WITH PASSWORD 'password'; -GRANT ALL PRIVILEGES ON DATABASE myproject TO myuser; -``` - -Prepare a new project: - -```bash -django-admin.py startproject PROJECT_NAME --template="orchestra/conf/ribaguifi_template" -``` - -Run migrations: -```bash -python3 manage.py migrate -``` - -(Optional) You can start a Django development server to check that everything is ok. -```bash -python3 manage.py runserver -``` - -Open [http://127.0.0.1:8000/](http://127.0.0.1:8000/) in your browser. diff --git a/install_manually.md b/install_manually.md index 51d8c660..a2747041 100644 --- a/install_manually.md +++ b/install_manually.md @@ -93,7 +93,7 @@ Remember create a database for your project and give permitions for the correct ``` psql -U postgres psql (12.4) -Digite «help». +Digite «help» para obtener ayuda. postgres=# CREATE database orchesta; postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta'; diff --git a/orchestra/conf/ribaguifi_template/locale/.gitignore b/orchestra/conf/ribaguifi_template/locale/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/orchestra/conf/ribaguifi_template/manage.py b/orchestra/conf/ribaguifi_template/manage.py deleted file mode 100755 index ee4b9652..00000000 --- a/orchestra/conf/ribaguifi_template/manage.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys - - -if __name__ == "__main__": - if sys.version_info < (3, 3): - cmd = ' '.join(sys.argv) - sys.stderr.write("Sorry, Orchestra requires at least Python 3.3, try with:\n$ python3 %s\n" % cmd) - sys.exit(1) - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") - from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/orchestra/conf/ribaguifi_template/media/.gitignore b/orchestra/conf/ribaguifi_template/media/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/orchestra/conf/ribaguifi_template/project_name/__init__.py b/orchestra/conf/ribaguifi_template/project_name/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/orchestra/conf/ribaguifi_template/project_name/settings.py b/orchestra/conf/ribaguifi_template/project_name/settings.py deleted file mode 100644 index 2fa397a5..00000000 --- a/orchestra/conf/ribaguifi_template/project_name/settings.py +++ /dev/null @@ -1,257 +0,0 @@ -""" -Django settings for {{ project_name }} project. - -Generated by 'django-admin startproject' using Django {{ django_version }}. - -For more information on this file, see -https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/ -""" - -import os -from decouple import config, Csv -from dj_database_url import parse as db_url - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '{{ secret_key }}' -# SECRET_KEY = config('SECRET_KEY') - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = config('DEBUG', default=False, cast=bool) -ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) - - -# Application definition - -INSTALLED_APPS = [ - # django-orchestra apps - 'orchestra', - 'orchestra.contrib.accounts', - 'orchestra.contrib.systemusers', - 'orchestra.contrib.contacts', - 'orchestra.contrib.orchestration', - 'orchestra.contrib.bills', - 'orchestra.contrib.payments', - 'orchestra.contrib.tasks', - 'orchestra.contrib.mailer', - 'orchestra.contrib.history', - 'orchestra.contrib.issues', - 'orchestra.contrib.services', - 'orchestra.contrib.plans', - 'orchestra.contrib.orders', - 'orchestra.contrib.domains', - 'orchestra.contrib.mailboxes', - 'orchestra.contrib.lists', - 'orchestra.contrib.webapps', - 'orchestra.contrib.websites', - 'orchestra.contrib.letsencrypt', - 'orchestra.contrib.databases', - 'orchestra.contrib.vps', - 'orchestra.contrib.saas', - 'orchestra.contrib.miscellaneous', - - # Third-party apps - 'django_extensions', - 'djcelery', - 'fluent_dashboard', - 'admin_tools', - 'admin_tools.theming', - 'admin_tools.menu', - 'admin_tools.dashboard', - 'rest_framework', - 'rest_framework.authtoken', - 'passlib.ext.django', - 'django_countries', -# 'debug_toolbar', - - # Django.contrib - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admin.apps.SimpleAdminConfig', - - # Last to load - 'orchestra.contrib.resources', - 'orchestra.contrib.settings', -# 'django_nose', -] - - -ROOT_URLCONF = '{{ project_name }}.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'orchestra.core.context_processors.site', - ], - 'loaders': [ - 'admin_tools.template_loaders.Loader', - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ], - }, - }, -] - - -WSGI_APPLICATION = '{{ project_name }}.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases - -DATABASES = { - 'default': config( - 'DATABASE_URL', - default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'), - cast=db_url - ) -} - - -# Internationalization -# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - - -try: - TIME_ZONE = open('/etc/timezone', 'r').read().strip() -except IOError: - TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/ - -STATIC_URL = '/static/' - - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -STATIC_ROOT = os.path.join(BASE_DIR, 'static') - -# Absolute filesystem path to the directory that will hold user-uploaded files. -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - - -# Path used for database translations files -LOCALE_PATHS = ( - os.path.join(BASE_DIR, 'locale'), -) - -ORCHESTRA_SITE_NAME = '{{ project_name }}' - - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - # 'django.middleware.locale.LocaleMiddleware' - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'orchestra.core.caches.RequestCacheMiddleware', - # also handles transations, ATOMIC_REQUESTS does not wrap middlewares - 'orchestra.contrib.orchestration.middlewares.OperationsMiddleware', -) - - -AUTH_USER_MODEL = 'accounts.Account' - - -AUTHENTICATION_BACKENDS = [ - 'orchestra.permissions.auth.OrchestraPermissionBackend', - 'django.contrib.auth.backends.ModelBackend', -] - - -EMAIL_BACKEND = 'orchestra.contrib.mailer.backends.EmailBackend' - - -# Needed for Bulk operations -DATA_UPLOAD_MAX_NUMBER_FIELDS = None - - -################################# -## 3RD PARTY APPS CONIGURATION ## -################################# - -# Admin Tools -ADMIN_TOOLS_MENU = 'orchestra.admin.menu.OrchestraMenu' - -# Fluent dashboard -ADMIN_TOOLS_INDEX_DASHBOARD = 'orchestra.admin.dashboard.OrchestraIndexDashboard' -FLUENT_DASHBOARD_ICON_THEME = '../orchestra/icons' - - -# Django-celery -import djcelery -djcelery.setup_loader() -CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' - - -# rest_framework -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ( - 'orchestra.permissions.api.OrchestraPermissionBackend', - ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.TokenAuthentication', - ), - 'DEFAULT_FILTER_BACKENDS': ( - ('django_filters.rest_framework.DjangoFilterBackend',) - ), -} - - -# Use a UNIX compatible hash -PASSLIB_CONFIG = ( - "[passlib]\n" - "schemes = sha512_crypt, django_pbkdf2_sha256, django_pbkdf2_sha1, " - " django_bcrypt, django_bcrypt_sha256, django_salted_sha1, des_crypt, " - " django_salted_md5, django_des_crypt, hex_md5, bcrypt, phpass\n" - "default = sha512_crypt\n" - "deprecated = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, " - " django_des_crypt, des_crypt, hex_md5\n" - "all__vary_rounds = 0.05\n" - "django_pbkdf2_sha256__min_rounds = 10000\n" - "sha512_crypt__min_rounds = 80000\n" - "staff__django_pbkdf2_sha256__default_rounds = 12500\n" - "staff__sha512_crypt__default_rounds = 100000\n" - "superuser__django_pbkdf2_sha256__default_rounds = 15000\n" - "superuser__sha512_crypt__default_rounds = 120000\n" -) - - -SHELL_PLUS_PRE_IMPORTS = ( - ('orchestra.contrib.orchestration.managers', ('orchestrate',)), -) diff --git a/orchestra/conf/ribaguifi_template/project_name/urls.py b/orchestra/conf/ribaguifi_template/project_name/urls.py deleted file mode 100644 index 3ae27421..00000000 --- a/orchestra/conf/ribaguifi_template/project_name/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.conf.urls import include, url - - -urlpatterns = [ - url(r'', include('orchestra.urls')), -] diff --git a/orchestra/conf/ribaguifi_template/project_name/wsgi.py b/orchestra/conf/ribaguifi_template/project_name/wsgi.py deleted file mode 100644 index 94d60c8c..00000000 --- a/orchestra/conf/ribaguifi_template/project_name/wsgi.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -WSGI config for {{ project_name }} project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/ -""" - -import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") - -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() diff --git a/total_requirements.txt b/total_requirements.txt deleted file mode 100644 index f360a3ef..00000000 --- a/total_requirements.txt +++ /dev/null @@ -1,39 +0,0 @@ -Django==1.10.5 -django-fluent-dashboard==0.6.1 -django-admin-tools==0.8.0 -django-extensions==1.7.4 -django-celery==3.1.17 -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 -django-iban==0.3.0 -requests -phonenumbers -django-countries -django-localflavor -amqp -anyjson -pytz -cracklib -lxml==3.3.5 -selenium -xvfbwrapper -freezegun==0.3.14 -coverage -flake8 -django-debug-toolbar==1.3.0 -django-nose==1.4.4 -sqlparse -pyinotify -PyMySQL -dj_database_url==0.5.0 -psycopg2-binary -python-decouple -https://github.com/glic3rinu/passlib/archive/master.zip From 9a3b6dcbc3fb8983c09a9ed56497d8dbde27730d Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 8 Jun 2021 10:23:04 +0200 Subject: [PATCH 03/12] Add 'exclude' attribute to TransactionSerializer Creating a ModelSerializer without either the 'fields' attribute or the 'exclude' attribute has been deprecated since 3.3.0 --- orchestra/contrib/payments/serializers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/orchestra/contrib/payments/serializers.py b/orchestra/contrib/payments/serializers.py index e423abbb..93ae9f78 100644 --- a/orchestra/contrib/payments/serializers.py +++ b/orchestra/contrib/payments/serializers.py @@ -10,7 +10,7 @@ class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedMod class Meta: model = PaymentSource fields = ('url', 'id', 'method', 'data', 'is_active') - + def validate(self, data): """ validate data according to method """ data = super(PaymentSourceSerializer, self).validate(data) @@ -20,7 +20,7 @@ class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedMod if not serializer.is_valid(): raise serializers.ValidationError(serializer.errors) return data - + def transform_data(self, obj, value): if not obj: return {} @@ -29,7 +29,7 @@ class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedMod serializer_class = plugin().get_serializer() return serializer_class().to_native(obj.data) return obj.data - + # TODO def metadata(self): meta = super(PaymentSourceSerializer, self).metadata() @@ -43,3 +43,4 @@ class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedMod class TransactionSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class Meta: model = Transaction + exclude = ('process',) From 81c67778e500fa9349841db29fe2b0c7ea4a6d4b Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 8 Jun 2021 12:58:36 +0200 Subject: [PATCH 04/12] Fix RelatedDomainSerializer model Regression introduced by 7d975637d53d3711b5f8fbe49241d685c4bcf98d partially fixed on 48ef1f21e3b679de756aa5aa5e1d2e8619e0c3b5 --- orchestra/contrib/mailboxes/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index 264afac8..36471996 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -8,7 +8,7 @@ from .models import Mailbox, Address class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: - model = Address.domain.field.model + model = Address.domain.field.related_model fields = ('url', 'id', 'name') From 70f7551e7d5b49a56cb72121c26c7138b7dabdd9 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 8 Jun 2021 13:34:36 +0200 Subject: [PATCH 05/12] Replace Router.get_default_base_name by Router.get_default_basename Deprecated in DRF 3.9.0 --- orchestra/api/options.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/orchestra/api/options.py b/orchestra/api/options.py index 8b377564..639f5927 100644 --- a/orchestra/api/options.py +++ b/orchestra/api/options.py @@ -18,33 +18,33 @@ class LogApiMixin(object): message = _('Added.') self.log(request, message, ADDITION, instance=self.serializer.instance) return response - + def perform_create(self, serializer): """ stores serializer for accessing instance on create() """ super(LogApiMixin, self).perform_create(serializer) self.serializer = serializer - + def update(self, request, *args, **kwargs): from django.contrib.admin.models import CHANGE response = super(LogApiMixin, self).update(request, *args, **kwargs) message = _('Changed data') self.log(request, message, CHANGE) return response - + def partial_update(self, request, *args, **kwargs): from django.contrib.admin.models import CHANGE response = super(LogApiMixin, self).partial_update(request, *args, **kwargs) message = _('Changed %s') % response.data self.log(request, message, CHANGE) return response - + def destroy(self, request, *args, **kwargs): from django.contrib.admin.models import DELETION message = _('Deleted') self.log(request, message, DELETION) response = super(LogApiMixin, self).destroy(request, *args, **kwargs) return response - + def log(self, request, message, action, instance=None): from django.contrib.admin.models import LogEntry instance = instance or self.get_object() @@ -64,21 +64,21 @@ class LinkHeaderRouter(DefaultRouter): APIRoot = import_class(settings.ORCHESTRA_API_ROOT_VIEW) APIRoot.router = self return APIRoot.as_view() - + def register(self, prefix, viewset, base_name=None): """ inserts link headers on every viewset """ if base_name is None: - base_name = self.get_default_base_name(viewset) + base_name = self.get_default_basename(viewset) insert_links(viewset, base_name) self.registry.append((prefix, viewset, base_name)) - + def get_viewset(self, prefix_or_model): for _prefix, viewset, __ in self.registry: if _prefix == prefix_or_model or viewset.queryset.model == prefix_or_model: return viewset msg = "%s does not have a regiestered viewset" % prefix_or_model raise KeyError(msg) - + def insert(self, prefix_or_model, name, field, **kwargs): """ Dynamically add new fields to an existing serializer """ viewset = self.get_viewset(prefix_or_model) From e1d71fa620d15572dbc1749ae1fe3bc36eb1daa9 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Tue, 8 Jun 2021 13:37:00 +0200 Subject: [PATCH 06/12] Add support to create Address via API --- orchestra/contrib/mailboxes/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index 36471996..c0ab4639 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -54,3 +54,13 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri if not attrs['mailboxes'] and not attrs['forward']: raise serializers.ValidationError("A mailbox or forward address should be provided.") return attrs + + def create(self, validated_data): + mailboxes = validated_data.pop('mailboxes') + + # assign address to same account than domain + account = validated_data['domain'].account + obj = self.Meta.model.objects.create(account=account, **validated_data) + + obj.mailboxes.set(mailboxes) + return obj From 867d9afe651cf977270f63f5180e5d92310701a8 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 18 Jun 2021 11:11:50 +0200 Subject: [PATCH 07/12] Make /aoi/addresses/ endpoint writable --- orchestra/contrib/mailboxes/serializers.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index c0ab4639..f7be6175 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -1,3 +1,4 @@ +from django.db import transaction from rest_framework import serializers from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer @@ -43,7 +44,7 @@ class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSe class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): domain = RelatedDomainSerializer() - mailboxes = RelatedMailboxSerializer(many=True, required=False) #allow_add_remove=True + mailboxes = RelatedMailboxSerializer(many=True, required=False) class Meta: model = Address @@ -51,16 +52,21 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri def validate(self, attrs): attrs = super(AddressSerializer, self).validate(attrs) - if not attrs['mailboxes'] and not attrs['forward']: + mailboxes = attrs.get('mailboxes', []) + forward = attrs.get('forward', '') + if not mailboxes and not forward: raise serializers.ValidationError("A mailbox or forward address should be provided.") return attrs + @transaction.atomic def create(self, validated_data): - mailboxes = validated_data.pop('mailboxes') - - # assign address to same account than domain - account = validated_data['domain'].account - obj = self.Meta.model.objects.create(account=account, **validated_data) - + mailboxes = validated_data.pop('mailboxes', []) + obj = super().create(validated_data) obj.mailboxes.set(mailboxes) return obj + + @transaction.atomic + def update(self, instance, validated_data): + mailboxes = validated_data.pop('mailboxes', []) + instance.mailboxes.set(mailboxes) + return super().update(instance, validated_data) From 7d6a2474ab1711502c464c0d29aad6f5fc713d10 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 8 Jul 2021 12:25:29 +0200 Subject: [PATCH 08/12] Handle missing url attribute on write requests --- orchestra/api/serializers.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/orchestra/api/serializers.py b/orchestra/api/serializers.py index 1fd1bb8c..577784f5 100644 --- a/orchestra/api/serializers.py +++ b/orchestra/api/serializers.py @@ -17,7 +17,7 @@ class SetPasswordSerializer(serializers.Serializer): class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): """ support for postonly_fields, fields whose value can only be set on post """ - + def validate(self, attrs): """ calls model.clean() """ attrs = super(HyperlinkedModelSerializer, self).validate(attrs) @@ -39,7 +39,7 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): instance = ModelClass(**validated_data) instance.clean() return attrs - + def post_only_cleanning(self, instance, validated_data): """ removes postonly_fields from attrs """ model_attrs = dict(**validated_data) @@ -49,12 +49,12 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): if attr in post_only_fields: model_attrs.pop(attr) return model_attrs - + def update(self, instance, validated_data): """ removes postonly_fields from attrs when not posting """ model_attrs = self.post_only_cleanning(instance, validated_data) return super(HyperlinkedModelSerializer, self).update(instance, model_attrs) - + def partial_update(self, instance, validated_data): """ removes postonly_fields from attrs when not posting """ model_attrs = self.post_only_cleanning(instance, validated_data) @@ -64,7 +64,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer): """ returns object on to_internal_value based on URL """ def to_internal_value(self, data): - url = data.get('url') + try: + url = data.get('url') + except AttributeError: + url = None if not url: raise ValidationError({ 'url': "URL is required." @@ -80,7 +83,7 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer): password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, required=False, style={'widget': widgets.PasswordInput}) - + def validate_password(self, attrs, source): """ POST only password """ if self.instance: @@ -89,7 +92,7 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer): elif 'password' not in attrs: raise serializers.ValidationError(_("Password required")) return attrs - + def validate(self, attrs): """ remove password in case is not a real model field """ try: @@ -102,7 +105,7 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer): if password is not None: attrs['password'] = password return attrs - + def create(self, validated_data): password = validated_data.pop('password') instance = self.Meta.model(**validated_data) From 008f49100f87685ce218fa8bb86dd9d152636081 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 24 Sep 2021 13:54:34 +0200 Subject: [PATCH 09/12] Fix display_mailboxes format (mark HTML as safe) --- orchestra/contrib/mailboxes/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orchestra/contrib/mailboxes/admin.py b/orchestra/contrib/mailboxes/admin.py index f1b54feb..3314b1db 100644 --- a/orchestra/contrib/mailboxes/admin.py +++ b/orchestra/contrib/mailboxes/admin.py @@ -252,7 +252,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): def display_mailboxes(self, address): boxes = address.mailboxes.all() return format_html_join( - '
', '{}', + mark_safe('
'), '{}', [(change_url(mailbox), mailbox.name) for mailbox in boxes] ) display_mailboxes.short_description = _("Mailboxes") @@ -261,7 +261,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): def display_all_mailboxes(self, address): boxes = address.get_mailboxes() return format_html_join( - '
', '{}', + mark_safe('
'), '{}', [(change_url(mailbox), mailbox.name) for mailbox in boxes] ) display_all_mailboxes.short_description = _("Mailboxes links") From 9a4f4ee17cdee7666206bf637f5e7d8f796ffa2d Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 7 Oct 2021 14:11:50 +0200 Subject: [PATCH 10/12] Fix SetPasswordHyperlinkedSerializer (update to new DRF) --- orchestra/api/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/orchestra/api/serializers.py b/orchestra/api/serializers.py index 577784f5..01005771 100644 --- a/orchestra/api/serializers.py +++ b/orchestra/api/serializers.py @@ -84,14 +84,14 @@ class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer): validators=[validate_password], write_only=True, required=False, style={'widget': widgets.PasswordInput}) - def validate_password(self, attrs, source): + def validate_password(self, value): """ POST only password """ if self.instance: - if 'password' in attrs: + if value: raise serializers.ValidationError(_("Can not set password")) - elif 'password' not in attrs: + elif not value: raise serializers.ValidationError(_("Password required")) - return attrs + return value def validate(self, attrs): """ remove password in case is not a real model field """ From e88e27a56e70deae5269c75620f5d68aba1b76e1 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 7 Oct 2021 14:14:21 +0200 Subject: [PATCH 11/12] Make MailboxViewSet writable: create & update --- orchestra/contrib/mailboxes/api.py | 8 +++++++- orchestra/contrib/mailboxes/serializers.py | 24 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/orchestra/contrib/mailboxes/api.py b/orchestra/contrib/mailboxes/api.py index 16d926d3..e17b68dc 100644 --- a/orchestra/contrib/mailboxes/api.py +++ b/orchestra/contrib/mailboxes/api.py @@ -4,7 +4,7 @@ from orchestra.api import router, SetPasswordApiMixin, LogApiMixin from orchestra.contrib.accounts.api import AccountApiMixin from .models import Address, Mailbox -from .serializers import AddressSerializer, MailboxSerializer +from .serializers import AddressSerializer, MailboxSerializer, MailboxWritableSerializer class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): @@ -17,6 +17,12 @@ class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets queryset = Mailbox.objects.prefetch_related('addresses__domain').all() serializer_class = MailboxSerializer + def get_serializer_class(self): + if self.request.method == 'GET': + return self.serializer_class + + return MailboxWritableSerializer + router.register(r'mailboxes', MailboxViewSet) router.register(r'addresses', AddressViewSet) diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index f7be6175..ad14b11c 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -36,6 +36,30 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer postonly_fields = ('name', 'password') +class MailboxWritableSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer): + addresses = serializers.HyperlinkedRelatedField(many=True, view_name='address-detail', queryset=Address.objects.all()) + + class Meta: + model = Mailbox + fields = ( + 'url', 'id', 'name', 'password', 'filtering', 'custom_filtering', 'addresses', 'is_active' + ) + postonly_fields = ('name', 'password') + + @transaction.atomic + def create(self, validated_data): + addresses = validated_data.pop('addresses', []) + instance = super().create(validated_data) + instance.addresses.set(addresses) + return instance + + @transaction.atomic + def update(self, instance, validated_data): + addresses = validated_data.pop('addresses', []) + instance.addresses.set(addresses) + return super().update(instance, validated_data) + + class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = Mailbox From 03666d8ed09aeaaa90cda8d7e9e8918e57049897 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 14 Oct 2021 13:03:08 +0200 Subject: [PATCH 12/12] Filter related addresses by account --- orchestra/contrib/mailboxes/serializers.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index ad14b11c..1608a6ce 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -36,8 +36,15 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer postonly_fields = ('name', 'password') +class AddressRelatedField(serializers.HyperlinkedRelatedField): + # Filter addresses by account (user) + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(account=self.context['account']) + + class MailboxWritableSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer): - addresses = serializers.HyperlinkedRelatedField(many=True, view_name='address-detail', queryset=Address.objects.all()) + addresses = AddressRelatedField(many=True, view_name='address-detail', queryset=Address.objects.all()) class Meta: model = Mailbox @@ -46,6 +53,10 @@ class MailboxWritableSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSe ) postonly_fields = ('name', 'password') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['addresses'].context['account'] = self.account + @transaction.atomic def create(self, validated_data): addresses = validated_data.pop('addresses', [])