diff --git a/.travis.yml b/.travis.yml index c306c1f9..6a3cca25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: # - "3.6" env: - - DJANGO_SECRET_KEY=0 OPENNEBULA_USERNAME='test' OPENNEBULA_PASSWORD='test' OPENNEBULA_PROTOCOL='http' OPENNEBULA_DOMAIN='test_domain' OPENNEBULA_PORT='2633' OPENNEBULA_ENDPOINT='/RPC2' DCL_TEXT='Data Center Light' CELERY_MAX_RETRIES=0 + - DJANGO_SECRET_KEY=0 OPENNEBULA_USERNAME='test' OPENNEBULA_PASSWORD='test' OPENNEBULA_PROTOCOL='http' OPENNEBULA_DOMAIN='test_domain' OPENNEBULA_PORT='2633' OPENNEBULA_ENDPOINT='/RPC2' DCL_TEXT='Data Center Light' CELERY_MAX_RETRIES=0 UNGLEICH_SITE_CONFIGS='{"localhost":{"MULTISITE_CMS_URL":"dynamicweb.urls"}}' # install dependencies install: "pip install -r requirements.txt" script: diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 47534585..b3bb0d20 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -5,6 +5,7 @@ Copyright 2015 ungleich. # -*- coding: utf-8 -*- # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os +import json from django.utils.translation import ugettext_lazy as _ @@ -54,7 +55,8 @@ PROJECT_DIR = os.path.abspath( # load .env file dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR)) -SITE_ID = 1 +from multisite import SiteID +SITE_ID = SiteID(default=1) APP_ROOT_ENDPOINT = "/" APPEND_SLASH = True @@ -76,6 +78,7 @@ SECRET_KEY = env('DJANGO_SECRET_KEY') INSTALLED_APPS = ( # 1st migrate 'membership', + 'djangocms_admin_style', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -83,6 +86,8 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', + 'multisite', + 'djangocms_multisite', 'easy_thumbnails', 'utils', 'stored_messages', @@ -124,7 +129,6 @@ INSTALLED_APPS = ( # 'djangocms_teaser', 'djangocms_page_meta', 'djangocms_text_ckeditor', - 'djangocms_admin_style', 'cmsplugin_filer_file', 'cmsplugin_filer_folder', 'cmsplugin_filer_link', @@ -163,6 +167,8 @@ MIDDLEWARE_CLASSES = ( 'cms.middleware.page.CurrentPageMiddleware', 'cms.middleware.toolbar.ToolbarMiddleware', 'cms.middleware.language.LanguageCookieMiddleware', + 'multisite.middleware.DynamicSiteMiddleware', + 'djangocms_multisite.middleware.CMSMultiSiteMiddleware', ) CSRF_FAILURE_VIEW = 'hosting.views.forbidden_view' @@ -328,6 +334,8 @@ CMS_PLACEHOLDER_CONF = { }, } +CMS_PERMISSION=True + CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', @@ -507,6 +515,36 @@ STRIPE_API_PRIVATE_KEY_TEST = env('STRIPE_API_PRIVATE_KEY_TEST') ANONYMOUS_USER_NAME = 'anonymous@ungleich.ch' GUARDIAN_GET_INIT_ANONYMOUS_USER = 'membership.models.get_anonymous_user_instance' +UNGLEICH_SITE_CONFIGS = env('UNGLEICH_SITE_CONFIGS') + +MULTISITE_CMS_URLS = {} +if UNGLEICH_SITE_CONFIGS == "": + raise Exception("Please define UNGLEICH_SITE_CONFIGS in your .env") +else: + try: + configs_dict=json.loads(UNGLEICH_SITE_CONFIGS) + except ValueError as verr: + raise Exception("UNGLEICH_SITE_CONFIGS is not a valid JSON: {}".format( + str(verr) + )) + else: + MULTISITE_CMS_URLS = { + k:v['MULTISITE_CMS_URL'] for (k,v) in configs_dict.items() + } + +MULTISITE_CMS_ALIASES = { +} +MULTISITE_CMS_FALLBACK = env('MULTISITE_CMS_FALLBACK') +if MULTISITE_CMS_FALLBACK == '': + MULTISITE_CMS_FALLBACK = 'datacenterlight.ch' +MULTISITE_FALLBACK = 'django.views.generic.base.RedirectView' +MULTISITE_FALLBACK_KWARGS = { + 'url': 'https://{}/'.format(MULTISITE_CMS_FALLBACK), 'permanent': False +} + +FILER_ENABLE_PERMISSIONS = True + + ############################################# # configurations for opennebula-integration # ############################################# diff --git a/dynamicweb/settings/local.py b/dynamicweb/settings/local.py index 1b03f3fe..4ea3dc7e 100644 --- a/dynamicweb/settings/local.py +++ b/dynamicweb/settings/local.py @@ -19,5 +19,6 @@ MIDDLEWARE_CLASSES += ("debug_toolbar.middleware.DebugToolbarMiddleware",) INSTALLED_APPS += ( 'django_extensions', - 'debug_toolbar' + # debug_toolbar seems to conflict with multisite (and djangocms_multisite) + # 'debug_toolbar' ) diff --git a/dynamicweb/urls_multi.py b/dynamicweb/urls_multi.py new file mode 100644 index 00000000..09bbb8dc --- /dev/null +++ b/dynamicweb/urls_multi.py @@ -0,0 +1,17 @@ +from django.conf import settings +from django.conf.urls import include, url +from django.conf.urls.i18n import i18n_patterns +from django.contrib import admin +from django.views import static as static_view + +urlpatterns = i18n_patterns( + url(r'^admin/', include(admin.site.urls)), + url(r'^ncms/', include('cms.urls')), +) + +urlpatterns += [ + url(r'^media/(?P.*)$', + static_view.serve, { + 'document_root': settings.MEDIA_ROOT, + }), +] diff --git a/membership/admin.py b/membership/admin.py index f69ccbef..3aefa780 100644 --- a/membership/admin.py +++ b/membership/admin.py @@ -1,20 +1,103 @@ +from django import forms from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.forms import ReadOnlyPasswordHashField + from .models import CustomUser, StripeCustomer -from django.contrib.auth.hashers import make_password -class CustomUserAdmin(admin.ModelAdmin): - fields = ('password', 'user_permissions', 'email', 'is_admin') +# Refer https://docs.djangoproject.com/en/2.0/topics/auth/customizing/ +# for understanding custom auth user model - def save_model(self, request, obj, form, change): - password = form.cleaned_data.get('password') - if not change: - obj.validation_slug = make_password(None) +class UserCreationForm(forms.ModelForm): + """A form for creating new users. Includes all the required + fields, plus a repeated password.""" + password1 = forms.CharField(label='Password', widget=forms.PasswordInput) + password2 = forms.CharField(label='Password confirmation', + widget=forms.PasswordInput) - obj.set_password(password) - obj.save() - return obj + class Meta: + model = CustomUser + fields = ('email', 'user_permissions', 'email', 'is_admin') + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError("Passwords don't match") + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super().save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + + +class UserChangeForm(forms.ModelForm): + """A form for updating users. Includes all the fields on + the user, but replaces the password field with admin's + password hash display field. + """ + password = ReadOnlyPasswordHashField( + label="Password", + help_text=( + "Raw passwords are not stored, so there is no way to see " + "this user's password, but you can change the password " + "using this form.") + ) + + class Meta: + model = CustomUser + fields = ('email', 'password', 'is_admin') + + def clean_password(self): + # Regardless of what the user provides, return the initial value. + # This is done here, rather than on the field, because the + # field does not have access to the initial value + return self.initial["password"] + + +class CustomUserAdmin(BaseUserAdmin): + # The forms to add and change user instances + form = UserChangeForm + add_form = UserCreationForm + + # The fields to be used in displaying the User model. + # These override the definitions on the base UserAdmin + # that reference specific fields on auth.User. + list_display = ( + 'email', 'is_admin', 'is_superuser' + ) + list_filter = () + fieldsets = ( + (None, {'fields': ('email',)}), + ('Change Password', + {'fields': ('password',), + 'description': "Raw passwords are not stored, so there is no way to " + "see this user's password, but you can change the " + "password using this " + "form." + } + ), + ('Permissions', {'fields': ('is_admin', 'user_permissions', + 'groups')}), + ) + # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin + # overrides get_fieldsets to use this attribute when creating a user. + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'password1', 'password2')} + ), + ) + search_fields = ('email',) + ordering = ('email',) + filter_horizontal = () admin.site.register(CustomUser, CustomUserAdmin) diff --git a/membership/migrations/0007_auto_20180213_0128.py b/membership/migrations/0007_auto_20180213_0128.py new file mode 100644 index 00000000..0dd7b54a --- /dev/null +++ b/membership/migrations/0007_auto_20180213_0128.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-02-13 01:28 +from __future__ import unicode_literals + +from django.db import migrations, models +import membership.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0006_auto_20160526_0445'), + ] + + operations = [ + migrations.AlterField( + model_name='customuser', + name='validation_slug', + field=models.CharField(db_index=True, default=membership.models.get_validation_slug, max_length=50, unique=True), + ), + ] diff --git a/membership/models.py b/membership/models.py index 73804008..b3cbcd91 100644 --- a/membership/models.py +++ b/membership/models.py @@ -59,6 +59,10 @@ class MyUserManager(BaseUserManager): return user +def get_validation_slug(): + return make_password(None) + + class CustomUser(AbstractBaseUser, PermissionsMixin): VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated')) site = models.ForeignKey(Site, default=1) @@ -66,8 +70,12 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(unique=True) validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0) - validation_slug = models.CharField(db_index=True, unique=True, - max_length=50) + # By default, we initialize the validation_slug with appropriate value + # This is required for User(page) admin + validation_slug = models.CharField( + db_index=True, unique=True, max_length=50, + default=get_validation_slug + ) is_admin = models.BooleanField( _('staff status'), default=False, @@ -171,6 +179,10 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): # Simplest possible answer: All admins are staff return self.is_admin + @is_staff.setter + def is_staff(self, value): + self._is_staff = value + class StripeCustomer(models.Model): user = models.OneToOneField(CustomUser) diff --git a/requirements.txt b/requirements.txt index 7a325357..85a41841 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,6 +34,7 @@ django-meta==1.2 django-meta-mixin==0.3.0 django-model-utils==2.5 django-mptt==0.8.4 +django-multisite==1.4.1 django-parler==1.6.3 django-phonenumber-field==1.1.0 django-polymorphic==0.9.2 @@ -97,3 +98,4 @@ billiard==3.5.0.3 amqp==2.2.1 vine==1.1.4 cdist==4.7.0 +git+https://github.com/ungleich/djangocms-multisite.git#egg=djangocms_multisite