diff --git a/TODO.md b/TODO.md index 1468bca8..e064b22f 100644 --- a/TODO.md +++ b/TODO.md @@ -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={}): diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index 3ea1bdc8..26460916 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -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) diff --git a/orchestra/apps/accounts/forms.py b/orchestra/apps/accounts/forms.py index 79ddfde1..b3907a63 100644 --- a/orchestra/apps/accounts/forms.py +++ b/orchestra/apps/accounts/forms.py @@ -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, }) diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index 28d404f0..23b4fea0 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -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): diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index fe87735d..0cf2d507 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -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 diff --git a/orchestra/apps/saas/settings.py b/orchestra/apps/saas/settings.py index c143b2f7..bb782819 100644 --- a/orchestra/apps/saas/settings.py +++ b/orchestra/apps/saas/settings.py @@ -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') diff --git a/orchestra/apps/services/admin.py b/orchestra/apps/services/admin.py index 6e645abf..7ff80239 100644 --- a/orchestra/apps/services/admin.py +++ b/orchestra/apps/services/admin.py @@ -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',), diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 5aeeeb75..d555375a 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -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 = { diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 6b1099d8..fc09fd49 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -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.
" "Defaults to '%s: %s' % (handler.description, instance)" )) + 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 trial periods"), + 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=_( diff --git a/orchestra/apps/services/settings.py b/orchestra/apps/services/settings.py index 3605bf35..835b47a6 100644 --- a/orchestra/apps/services/settings.py +++ b/orchestra/apps/services/settings.py @@ -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') diff --git a/orchestra/apps/webapps/backends/__init__.py b/orchestra/apps/webapps/backends/__init__.py index 5173b5fd..b96a592a 100644 --- a/orchestra/apps/webapps/backends/__init__.py +++ b/orchestra/apps/webapps/backends/__init__.py @@ -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) diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index aa7b9e92..03eba5a1 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -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: diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index 11c7fc8f..2d35dd86 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -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/\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/\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/\n" - "Apache-mod-fcgid will be used to execute PHP files.") - ), - 'static': ( - _("Static"), - 'alias', (), - _("This creates a Static application under ~/webapps/\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://.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://.drupal.example.com/install.php?profile=standard&locale=ca\n" -# "By default this site will be accessible via http://.drupal.example.com") -# ), - 'webalizer': ( - _("Webalizer"), - 'alias', ('%(app_path)s%(site_name)s',), - _("This creates a Webalizer application under ~/webapps/-\n") - ), + 'directive': ('fpm', 'fcgi://{}%(app_path)s'.format(WEBAPPS_FPM_LISTEN)), + 'help_text': _("This creates a PHP5.5 application under ~/webapps/<app_name>
" + "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/<app_name>
" + "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/<app_name>
" + "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/<app_name>
" + "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/-") + }, }) @@ -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') - diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index 54a7dc04..1924fd69 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -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 ! diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index d6a4737a..372aa98f 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -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: diff --git a/orchestra/forms/options.py b/orchestra/forms/options.py index b509763a..5ae8ad9d 100644 --- a/orchestra/forms/options.py +++ b/orchestra/forms/options.py @@ -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 diff --git a/orchestra/settings.py b/orchestra/settings.py index 0663969d..268d3889 100644 --- a/orchestra/settings.py +++ b/orchestra/settings.py @@ -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) diff --git a/orchestra/utils/humanize.py b/orchestra/utils/humanize.py index 855b12b5..2794a313 100644 --- a/orchestra/utils/humanize.py +++ b/orchestra/utils/humanize.py @@ -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 + diff --git a/orchestra/utils/options.py b/orchestra/utils/options.py index 02deedde..0e52ad1b 100644 --- a/orchestra/utils/options.py +++ b/orchestra/utils/options.py @@ -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] + )