Random fixes

This commit is contained in:
Marc 2014-10-24 10:16:46 +00:00
parent d5cc1b7d7c
commit 786b9a7657
19 changed files with 203 additions and 129 deletions

View File

@ -160,9 +160,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* service.name / verbose_name instead of .description ?
* miscellaneous.name / verbose_name
* service.invoice_name
* Bills can have sublines?
* proforma without billing contact?
@ -177,3 +174,5 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* ManyToManyField.symmetrical = False (user group)
* REST PERMISSIONS
* caching based on def text2int(textnum, numwords={}):

View File

@ -42,7 +42,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
)
fieldsets = (
(_("User"), {
'fields': ('username', 'password',)
'fields': ('username', 'password', 'main_systemuser_link')
}),
(_("Personal info"), {
'fields': ('first_name', 'last_name', 'email', ('type', 'language'), 'comments'),
@ -59,12 +59,14 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
add_form = AccountCreationForm
form = UserChangeForm
filter_horizontal = ()
change_readonly_fields = ('username',)
change_readonly_fields = ('username', 'main_systemuser_link')
change_form_template = 'admin/accounts/account/change_form.html'
actions = [disable]
change_view_actions = actions
list_select_related = ('billcontact',)
main_systemuser_link = admin_link('main_systemuser')
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'comments':
@ -101,9 +103,11 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
return fieldsets
def save_model(self, request, obj, form, change):
super(AccountAdmin, self).save_model(request, obj, form, change)
if not change:
form.save_model(obj)
form.save_related(obj)
else:
super(AccountAdmin, self).save_model(request, obj, form, change)
admin.site.register(Account, AccountAdmin)

View File

@ -1,3 +1,5 @@
from collections import OrderedDict
from django import forms
from django.db.models.loading import get_model
from django.utils.translation import ugettext_lazy as _
@ -9,12 +11,12 @@ from .models import Account
def create_account_creation_form():
fields = {
'create_systemuser': forms.BooleanField(initial=True, required=False,
label=_("Create systemuser"), widget=forms.CheckboxInput(attrs={'disabled': True}),
help_text=_("Designates whether to creates a related system user with the same "
"username and password or not."))
}
fields = OrderedDict(**{
'enable_systemuser': forms.BooleanField(initial=True, required=False,
label=_("Enable systemuser"),
help_text=_("Designates whether to creates an enabled or disabled related system user. "
"Notice that a related system user will be always created."))
})
for model, key, kwargs, help_text in settings.ACCOUNTS_CREATE_RELATED:
model = get_model(model)
field_name = 'create_%s' % model._meta.model_name
@ -46,6 +48,8 @@ def create_account_creation_form():
raise forms.ValidationError(
_("A %s with this name already exists") % verbose_name
)
def save_model(self, account):
account.save(active_systemuser=self.cleaned_data['enable_systemuser'])
def save_related(self, account):
for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
@ -60,6 +64,7 @@ def create_account_creation_form():
fields.update({
'create_related_fields': fields.keys(),
'clean': clean,
'save_model': save_model,
'save_related': save_related,
})

View File

@ -60,12 +60,12 @@ class Account(auth.AbstractBaseUser):
def get_main(cls):
return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK)
def save(self, *args, **kwargs):
def save(self, active_systemuser=False, *args, **kwargs):
created = not self.pk
super(Account, self).save(*args, **kwargs)
if created:
self.main_systemuser = self.systemusers.create(account=self, username=self.username,
password=self.password)
password=self.password, is_active=active_systemuser)
self.save(update_fields=['main_systemuser'])
def clean(self):

View File

@ -146,10 +146,7 @@ class Order(models.Model):
if account_id is None:
# New account workaround -> user.account_id == None
continue
ignore = False
account = getattr(instance, 'account', instance)
if account.is_superuser:
ignore = service.ignore_superusers
ignore = service.handler.get_ignore(instance)
order = cls(content_object=instance, service=service,
account_id=account_id, ignore=ignore)
if commit:
@ -163,8 +160,7 @@ class Order(models.Model):
order.update()
elif orders:
order = orders.get()
if commit:
order.cancel()
order.cancel(commit=commit)
updates.append((order, 'cancelled'))
return updates
@ -188,10 +184,12 @@ class Order(models.Model):
self.description = description
self.save(update_fields=['description'])
def cancel(self):
def cancel(self, commit=True):
self.cancelled_on = timezone.now()
self.save(update_fields=['cancelled_on'])
logger.info("CANCELLED order id: {id}".format(id=self.id))
self.ignore = self.service.handler.get_order_ignore(self)
if commit:
self.save(update_fields=['cancelled_on', 'ignore'])
logger.info("CANCELLED order id: {id}".format(id=self.id))
def mark_as_ignored(self):
self.ignore = True

View File

@ -10,3 +10,23 @@ SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', (
'orchestra.apps.saas.services.gitlab.GitLabService',
'orchestra.apps.saas.services.phplist.PHPListService',
))
SAAS_WORDPRESSMU_BASE_URL = getattr(settings, 'SAAS_WORDPRESSMU_BASE_URL',
'http://%(site_name)s.example.com')
SAAS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'SAAS_WORDPRESSMU_ADMIN_PASSWORD',
'secret')
SAAS_DOKUWIKIMU_TEMPLATE_PATH = setattr(settings, 'SAAS_DOKUWIKIMU_TEMPLATE_PATH',
'/home/httpd/htdocs/wikifarm/template.tar.gz')
SAAS_DOKUWIKIMU_FARM_PATH = getattr(settings, 'SAAS_DOKUWIKIMU_FARM_PATH',
'/home/httpd/htdocs/wikifarm/farm')
SAAS_DRUPAL_SITES_PATH = getattr(settings, 'SAAS_DRUPAL_SITES_PATH',
'/home/httpd/htdocs/drupal-mu/sites/%(site_name)s')

View File

@ -42,7 +42,8 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
}),
(_("Billing options"), {
'classes': ('wide',),
'fields': ('billing_period', 'billing_point', 'is_fee', 'order_description')
'fields': ('billing_period', 'billing_point', 'is_fee', 'order_description',
'ignore_period')
}),
(_("Pricing options"), {
'classes': ('wide',),

View File

@ -55,6 +55,32 @@ class ServiceHandler(plugins.Plugin):
}
return eval(self.match, safe_locals)
def get_ignore_delta(self):
if self.ignore_period == self.NEVER:
return None
value, unit = self.ignore_period.split('_')
value = text2int(value)
if unit.lowe().startswith('day'):
return timedelta(days=value)
if unit.lowe().startswith('month'):
return timedelta(months=value)
else:
raise ValueError("Unknown unit %s" % unit)
def get_order_ignore(self, order):
""" service trial delta """
ignore_delta = self.get_ignore_delta()
if ignore_delta and (order.cancelled_on-ignore_delta).date() <= order.registered_on:
return True
return order.ignore
def get_ignore(self, instance):
ignore = False
account = getattr(instance, 'account', instance)
if account.is_superuser:
ignore = self.ignore_superusers
return ignore
def get_metric(self, instance):
if self.metric:
safe_locals = {

View File

@ -89,6 +89,8 @@ class Service(models.Model):
# DAILY = 'DAILY'
MONTHLY = 'MONTHLY'
ANUAL = 'ANUAL'
ONE_DAY = 'ONE_DAY'
TWO_DAYS = 'TWO_DAYS'
TEN_DAYS = 'TEN_DAYS'
ONE_MONTH = 'ONE_MONTH'
ALWAYS = 'ALWAYS'
@ -158,6 +160,17 @@ class Service(models.Model):
"used for generating the description for the bill lines of this services.<br>"
"Defaults to <tt>'%s: %s' % (handler.description, instance)</tt>"
))
ignore_period = models.CharField(_("ignore period"), max_length=16, blank=True,
help_text=_("Period in which orders will be ignored if cancelled. "
"Useful for designating <i>trial periods</i>"),
choices=(
(NEVER, _("No ignore")),
(ONE_DAY, _("One day")),
(TWO_DAYS, _("Two days")),
(TEN_DAYS, _("Ten days")),
(ONE_MONTH, _("One month")),
),
default=settings.SERVICES_DEFAULT_IGNORE_PERIOD)
# Pricing
metric = models.CharField(_("metric"), max_length=256, blank=True,
help_text=_(

View File

@ -1,3 +1,5 @@
from datetime import timedelta
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
@ -14,3 +16,6 @@ SERVICES_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'SERVICES_SERVICE_ANUAL
SERVICES_ORDER_MODEL = getattr(settings, 'SERVICES_ORDER_MODEL', 'orders.Order')
SERVICES_DEFAULT_IGNORE_PERIOD = getattr(settings, 'SERVICES_DEFAULT_IGNORE_PERIOD', 'TWO_DAYS')

View File

@ -55,7 +55,6 @@ class WebAppServiceMixin(object):
}
for __, module_name, __ in pkgutil.walk_packages(__path__):
# sorry for the exec(), but Import module function fails :(
exec('from . import %s' % module_name)

View File

@ -5,23 +5,17 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.core import validators, services
from orchestra.utils import tuple_setting_to_choices, dict_setting_to_choices
from orchestra.utils.functional import cached
from . import settings
def settings_to_choices(choices):
return sorted(
[ (name, opt[0]) for name,opt in choices.iteritems() ],
key=lambda e: e[0]
)
class WebApp(models.Model):
""" Represents a web application """
name = models.CharField(_("name"), max_length=128, validators=[validators.validate_name])
type = models.CharField(_("type"), max_length=32,
choices=settings_to_choices(settings.WEBAPPS_TYPES),
choices=dict_setting_to_choices(settings.WEBAPPS_TYPES),
default=settings.WEBAPPS_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='webapps')
@ -41,20 +35,23 @@ class WebApp(models.Model):
def get_fpm_port(self):
return settings.WEBAPPS_FPM_START_PORT + self.account.pk
def get_method(self):
method = settings.WEBAPPS_TYPES[self.type]
args = method[2] if len(method) == 4 else ()
return method[1], args
def get_directive(self):
directive = settings.WEBAPPS_TYPES[self.type]['directive']
args = directive[1:] if len(directive) > 1 else ()
return directive[0], args
def get_path(self):
context = {
'user': self.account.username,
'home': webapp.get_user().get_home(),
'app_name': self.name,
}
return settings.WEBAPPS_BASE_ROOT % context
def get_user(self):
return self.account.main_systemuser
def get_username(self):
return self.account.username
return self.get_user().username
def get_groupname(self):
return self.get_username()
@ -64,7 +61,7 @@ class WebAppOption(models.Model):
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"),
related_name='options')
name = models.CharField(_("name"), max_length=128,
choices=settings_to_choices(settings.WEBAPPS_OPTIONS))
choices=tuple_setting_to_choices(settings.WEBAPPS_OPTIONS))
value = models.CharField(_("value"), max_length=256)
class Meta:

View File

@ -2,8 +2,7 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
# TODO make '%(mainuser_home)s/webapps...
WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '/home/%(user)s/webapps/%(app_name)s/')
WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '%(home)s/webapps/%(app_name)s/')
WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN',
@ -23,57 +22,36 @@ WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH',
WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', {
# { name: ( verbose_name, method_name, method_args, description) }
'php5.5': (
_("PHP 5.5"),
'php5.5': {
'verbose_name': "PHP 5.5",
# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',),
'fpm', ('fcgi://127.0.0.1:%(fpm_port)s%(app_path)s',),
_("This creates a PHP5.5 application under ~/webapps/<app_name>\n"
"PHP-FPM will be used to execute PHP files.")
),
'php5.2': (
_("PHP 5.2"),
'fcgid', (WEBAPPS_FCGID_PATH,),
_("This creates a PHP5.2 application under ~/webapps/<app_name>\n"
"Apache-mod-fcgid will be used to execute PHP files.")
),
'php4': (
_("PHP 4"),
'fcgid', (WEBAPPS_FCGID_PATH,),
_("This creates a PHP4 application under ~/webapps/<app_name>\n"
"Apache-mod-fcgid will be used to execute PHP files.")
),
'static': (
_("Static"),
'alias', (),
_("This creates a Static application under ~/webapps/<app_name>\n"
"Apache2 will be used to serve static content and execute CGI files.")
),
# 'wordpress': (
# _("Wordpress"),
# 'fpm', ('fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/',),
# _("This creates a Wordpress site into a shared Wordpress server\n"
# "By default this blog will be accessible via http://<app_name>.blogs.example.com")
#
# ),
# 'dokuwiki': (
# _("DokuWiki"),
# 'alias', ('/home/httpd/wikifarm/farm/',),
# _("This create a Dokuwiki wiki into a shared Dokuwiki server\n")
# ),
# 'drupal': (
# _("Drupdal"),
# 'fpm', ('fcgi://127.0.0.1:8991/home/httpd/drupal-mu/',),
# _("This creates a Drupal site into a shared Drupal server\n"
# "The installation will be completed after visiting "
# "http://<app_name>.drupal.example.com/install.php?profile=standard&locale=ca\n"
# "By default this site will be accessible via http://<app_name>.drupal.example.com")
# ),
'webalizer': (
_("Webalizer"),
'alias', ('%(app_path)s%(site_name)s',),
_("This creates a Webalizer application under ~/webapps/<app_name>-<site_name>\n")
),
'directive': ('fpm', 'fcgi://{}%(app_path)s'.format(WEBAPPS_FPM_LISTEN)),
'help_text': _("This creates a PHP5.5 application under ~/webapps/&lt;app_name&gt;<br>"
"PHP-FPM will be used to execute PHP files.")
},
'php5.2': {
'verbose_name': "PHP 5.2",
'directive': ('fcgi', WEBAPPS_FCGID_PATH),
'help_text': _("This creates a PHP5.2 application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.")
},
'php4': {
'verbose_name': "PHP 4",
'directive': ('fcgi', WEBAPPS_FCGID_PATH,),
'help_text': _("This creates a PHP4 application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.")
},
'static': {
'verbose_name': _("Static"),
'directive': ('static',),
'help_text': _("This creates a Static application under ~/webapps/&lt;app_name&gt;<br>"
"Apache2 will be used to serve static content and execute CGI files.")
},
'webalizer': {
'verbose_name': "Webalizer",
'directive': ('static', '%(app_path)s%(site_name)s'),
'help_text': _("This creates a Webalizer application under ~/webapps/<app_name>-<site_name>")
},
})
@ -194,25 +172,3 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO
'escapeshellarg',
'dl'
])
# TODO
WEBAPPS_WORDPRESSMU_BASE_URL = getattr(settings, 'WEBAPPS_WORDPRESSMU_BASE_URL',
'http://blogs.example.com')
WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD',
'secret')
WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH = setattr(settings, 'WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH',
'/home/httpd/htdocs/wikifarm/template.tar.gz')
WEBAPPS_DOKUWIKIMU_FARM_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_FARM_PATH',
'/home/httpd/htdocs/wikifarm/farm')
WEBAPPS_DRUPAL_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPAL_SITES_PATH',
'/home/httpd/htdocs/drupal-mu/sites/%(app_name)s')

View File

@ -65,12 +65,12 @@ class Apache2Backend(ServiceController):
def get_content_directives(self, site):
directives = ''
for content in site.content_set.all().order_by('-path'):
method, args = content.webapp.get_method()
method, args = content.webapp.get_directive()
method = getattr(self, 'get_%s_directives' % method)
directives += method(content, *args)
return directives
def get_alias_directives(self, content, *args):
def get_static_directives(self, content, *args):
context = self.get_content_context(content)
context['path'] = args[0] % context if args else content.webapp.get_path()
return "Alias %(location)s %(path)s\n" % context
@ -81,10 +81,10 @@ class Apache2Backend(ServiceController):
directive = "ProxyPassMatch ^%(location)s(.*\.php(/.*)?)$ %(fcgi_path)s$1\n"
return directive % context
def get_fcgid_directives(self, content, fcgid_path):
def get_fcgi_directives(self, content, fcgid_path):
context = self.get_content_context(content)
context['fcgid_path'] = fcgid_path % context
fcgid = self.get_alias_directives(content)
fcgid = self.get_static_directives(content)
fcgid += textwrap.dedent("""\
ProxyPass %(location)s !
<Directory %(app_path)s>

View File

@ -5,18 +5,12 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.core import validators, services
from orchestra.utils import tuple_setting_to_choices
from orchestra.utils.functional import cached
from . import settings
def settings_to_choices(choices):
return sorted(
[ (name, opt[0]) for name,opt in choices.iteritems() ],
key=lambda e: e[0]
)
class Website(models.Model):
name = models.CharField(_("name"), max_length=128, unique=True,
validators=[validators.validate_name])
@ -67,7 +61,7 @@ class WebsiteOption(models.Model):
website = models.ForeignKey(Website, verbose_name=_("web site"),
related_name='options')
name = models.CharField(_("name"), max_length=128,
choices=settings_to_choices(settings.WEBSITES_OPTIONS))
choices=tuple_setting_to_choices(settings.WEBSITES_OPTIONS))
value = models.CharField(_("value"), max_length=256)
class Meta:

View File

@ -2,6 +2,7 @@ from django import forms
from django.contrib.auth import forms as auth_forms
from django.utils.translation import ugettext, ugettext_lazy as _
from .. import settings
from ..core.validators import validate_password
@ -51,8 +52,8 @@ class UserCreationForm(forms.ModelForm):
# self.fields['password1'].validators.append(validate_password)
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError(
self.error_messages['password_mismatch'],
@ -72,7 +73,10 @@ class UserCreationForm(forms.ModelForm):
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if settings.ORCHESTRA_MIGRATION_MODE:
user.password = self.cleaned_data['password1']
else:
user.set_password(self.cleaned_data['password1'])
if commit:
user.save()
return user

View File

@ -28,3 +28,6 @@ STOP_SERVICES = getattr(settings, 'STOP_SERVICES',
API_ROOT_VIEW = getattr(settings, 'API_ROOT_VIEW', 'orchestra.api.root.APIRoot')
ORCHESTRA_MIGRATION_MODE = getattr(settings, 'ORCHESTRA_MIGRATION_MODE', False)

View File

@ -128,3 +128,39 @@ def naturaldate(date):
count = abs(count)
fmt = pluralizefun(count)
return fmt.format(num=count, ago=ago)
def text2int(textnum, numwords={}):
if not numwords:
units = (
'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen',
'sixteen', 'seventeen', 'eighteen', 'nineteen',
)
tens = ('', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety')
scales = ['hundred', 'thousand', 'million', 'billion', 'trillion']
numwords['and'] = (1, 0)
for idx, word in enumerate(units):
numwords[word] = (1, idx)
for idx, word in enumerate(tens):
numwords[word] = (1, idx * 10)
for idx, word in enumerate(scales):
numwords[word] = (10 ** (idx * 3 or 2), 0)
current = result = 0
for word in textnum.split():
word = word.lower()
if word not in numwords:
raise Exception("Illegal word: " + word)
scale, increment = numwords[word]
current = current * scale + increment
if scale > 100:
result += current
current = 0
return result + current

View File

@ -45,3 +45,17 @@ def running_syncdb():
def database_ready():
return not running_syncdb() and 'setuppostgres' not in sys.argv and 'test' not in sys.argv
def dict_setting_to_choices(choices):
return sorted(
[ (name, opt.get('verbose_name', 'name')) for name, opt in choices.iteritems() ],
key=lambda e: e[0]
)
def tuple_setting_to_choices(choices):
return sorted(
[ (name, opt[0]) for name,opt in choices.iteritems() ],
key=lambda e: e[0]
)