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 -U postgres
psql (12.4) psql (12.4)
Digite «help». Digite «help» para obtener ayuda.
postgres=# CREATE database orchesta; postgres=# CREATE database orchesta;
postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta'; postgres=# CREATE USER orchesta WITH PASSWORD 'orquesta';

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

View file

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

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): def display_mailboxes(self, address):
boxes = address.mailboxes.all() boxes = address.mailboxes.all()
return format_html_join( return format_html_join(
'<br>', '<a href="{}">{}</a>', mark_safe('<br>'), '<a href="{}">{}</a>',
[(change_url(mailbox), mailbox.name) for mailbox in boxes] [(change_url(mailbox), mailbox.name) for mailbox in boxes]
) )
display_mailboxes.short_description = _("Mailboxes") display_mailboxes.short_description = _("Mailboxes")
@ -261,7 +261,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def display_all_mailboxes(self, address): def display_all_mailboxes(self, address):
boxes = address.get_mailboxes() boxes = address.get_mailboxes()
return format_html_join( return format_html_join(
'<br>', '<a href="{}">{}</a>', mark_safe('<br>'), '<a href="{}">{}</a>',
[(change_url(mailbox), mailbox.name) for mailbox in boxes] [(change_url(mailbox), mailbox.name) for mailbox in boxes]
) )
display_all_mailboxes.short_description = _("Mailboxes links") 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 orchestra.contrib.accounts.api import AccountApiMixin
from .models import Address, Mailbox from .models import Address, Mailbox
from .serializers import AddressSerializer, MailboxSerializer from .serializers import AddressSerializer, MailboxSerializer, MailboxWritableSerializer
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
@ -17,6 +17,12 @@ class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets
queryset = Mailbox.objects.prefetch_related('addresses__domain').all() queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
serializer_class = MailboxSerializer serializer_class = MailboxSerializer
def get_serializer_class(self):
if self.request.method == 'GET':
return self.serializer_class
return MailboxWritableSerializer
router.register(r'mailboxes', MailboxViewSet) router.register(r'mailboxes', MailboxViewSet)
router.register(r'addresses', AddressViewSet) router.register(r'addresses', AddressViewSet)

View file

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

View file

@ -43,3 +43,4 @@ class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedMod
class TransactionSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class TransactionSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Transaction 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