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/api/serializers.py b/orchestra/api/serializers.py index 1fd1bb8c..01005771 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,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) diff --git a/orchestra/conf/project_template/project_name/settings.py b/orchestra/conf/project_template/project_name/settings.py index c59610d7..82b244f9 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', 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/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") 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 264afac8..1608a6ce 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 @@ -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) 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',) 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