Merge pull request #12 from ribaguifi/dev/api-writable

Update some API endpoints to make it writable
This commit is contained in:
Santiago L 2021-11-24 11:03:17 +01:00 committed by GitHub
commit 5e6cd2f147
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 84 additions and 426 deletions

View File

@ -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

View File

@ -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.

View File

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

View File

@ -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,16 +83,16 @@ 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):
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 """
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)

View File

@ -66,6 +66,7 @@ INSTALLED_APPS = [
'admin_tools.dashboard',
'rest_framework',
'rest_framework.authtoken',
'django_filters',
'passlib.ext.django',
'django_countries',
# 'debug_toolbar',

View File

@ -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)

View File

@ -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',)),
)

View File

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

View File

@ -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()

View File

@ -252,7 +252,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def display_mailboxes(self, address):
boxes = address.mailboxes.all()
return format_html_join(
'<br>', '<a href="{}">{}</a>',
mark_safe('<br>'), '<a href="{}">{}</a>',
[(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(
'<br>', '<a href="{}">{}</a>',
mark_safe('<br>'), '<a href="{}">{}</a>',
[(change_url(mailbox), mailbox.name) for mailbox in boxes]
)
display_all_mailboxes.short_description = _("Mailboxes links")

View File

@ -4,7 +4,7 @@ from orchestra.api import router, SetPasswordApiMixin, LogApiMixin
from orchestra.contrib.accounts.api import AccountApiMixin
from .models import Address, Mailbox
from .serializers import AddressSerializer, MailboxSerializer
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)

View File

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

View File

@ -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',)

View File

@ -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