diff --git a/TODO.md b/TODO.md index 0fd3659e..91cae3f7 100644 --- a/TODO.md +++ b/TODO.md @@ -186,7 +186,6 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl * use server.name | server.address on python backends, like gitlab instead of settings? * TODO raise404, here and everywhere -# display subline links on billlines, to show that they exists. * update service orders on a celery task? because it take alot # billline quantity eval('10x100') instead of miningless description '(10*100)' line.verbose_quantity @@ -246,7 +245,6 @@ celery max-tasks-per-child * autoscale celery workers http://docs.celeryproject.org/en/latest/userguide/workers.html#autoscaling -* webapp has_website list filter glic3rinu's django-fluent-dashboard * gevent is not ported to python3 :'( @@ -294,3 +292,6 @@ https://code.djangoproject.com/ticket/24576 * fpm reload starts new pools? * rename resource.monitors to resource.backends ? * abstract model classes enabling overriding? + +# Ignore superusers & co on billing +# bill.totals make it 100% computed? diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index b3cc03e5..cf4ad24e 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -36,7 +36,7 @@ def get_modeladmin(model, import_module=True): def insertattr(model, name, value): """ Inserts attribute to a modeladmin """ modeladmin = None - if models.Model in model.__mro__: + if isinstance(model, models.Model) modeladmin = get_modeladmin(model) modeladmin_class = type(modeladmin) elif not inspect.isclass(model): diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 74448d89..c8b0d887 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -4,6 +4,8 @@ from django.contrib import admin from django.contrib.admin.utils import unquote from django.core.urlresolvers import reverse from django.db import models +from django.db.models import F, Sum +from django.db.models.functions import Coalesce from django.templatetags.static import static from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -58,7 +60,9 @@ class ClosedBillLineInline(BillLineInline): # TODO reimplement as nested inlines when upstream # https://code.djangoproject.com/ticket/9025 - fields = ('display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total') + fields = ( + 'display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total' + ) readonly_fields = fields def display_description(self, line): @@ -77,6 +81,11 @@ class ClosedBillLineInline(BillLineInline): display_subtotal.short_description = _("Subtotal") display_subtotal.allow_tags = True + def display_total(self, line): + return line.get_total() + display_total.short_description = _("Total") + display_total.allow_tags = True + def has_add_permission(self, request): return False @@ -134,6 +143,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): change_view_actions = [ actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills ] + search_fields = ('number', 'account__username', 'comments') actions = [actions.download_bills, actions.close_bills, actions.send_bills] change_readonly_fields = ('account_link', 'type', 'is_open') readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state') @@ -147,10 +157,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): num_lines.short_description = _("lines") def display_total(self, bill): - return "%s &%s;" % (bill.total, settings.BILLS_CURRENCY.lower()) + return "%s &%s;" % (round(bill.totals, 2), settings.BILLS_CURRENCY.lower()) display_total.allow_tags = True display_total.short_description = _("total") - display_total.admin_order_field = 'total' + display_total.admin_order_field = 'totals' def type_link(self, bill): bill_type = bill.type.lower() @@ -210,8 +220,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): def get_inline_instances(self, request, obj=None): inlines = super(BillAdmin, self).get_inline_instances(request, obj) if obj and not obj.is_open: - return [inline for inline in inlines if not isinstance(inline, BillLineInline)] - return [inline for inline in inlines if not isinstance(inline, ClosedBillLineInline)] + return [inline for inline in inlines if type(inline) is not BillLineInline] + return [inline for inline in inlines if type(inline) is not ClosedBillLineInline] def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ @@ -223,8 +233,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): def get_queryset(self, request): qs = super(BillAdmin, self).get_queryset(request) - qs = qs.annotate(models.Count('lines')) - qs = qs.prefetch_related('lines', 'lines__sublines', 'transactions') + qs = qs.annotate( + models.Count('lines'), + totals=Sum( + (F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100) + ), + ) + qs = qs.prefetch_related('transactions') return qs def change_view(self, request, object_id, **kwargs): diff --git a/orchestra/contrib/bills/migrations/0001_initial.py b/orchestra/contrib/bills/migrations/0001_initial.py new file mode 100644 index 00000000..690c799a --- /dev/null +++ b/orchestra/contrib/bills/migrations/0001_initial.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings +import django.db.models.deletion +import django.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Bill', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), + ('number', models.CharField(verbose_name='number', blank=True, unique=True, max_length=16)), + ('type', models.CharField(verbose_name='type', choices=[('INVOICE', 'Invoice'), ('AMENDMENTINVOICE', 'Amendment invoice'), ('FEE', 'Fee'), ('AMENDMENTFEE', 'Amendment Fee'), ('PROFORMA', 'Pro forma')], max_length=16)), + ('created_on', models.DateField(verbose_name='created on', auto_now_add=True)), + ('closed_on', models.DateField(verbose_name='closed on', null=True, blank=True)), + ('is_open', models.BooleanField(verbose_name='open', default=True)), + ('is_sent', models.BooleanField(verbose_name='sent', default=False)), + ('due_on', models.DateField(verbose_name='due on', null=True, blank=True)), + ('updated_on', models.DateField(verbose_name='updated on', auto_now=True)), + ('total', models.DecimalField(default=0, decimal_places=2, max_digits=12)), + ('comments', models.TextField(verbose_name='comments', blank=True)), + ('html', models.TextField(verbose_name='HTML', blank=True)), + ('account', models.ForeignKey(verbose_name='account', related_name='bill', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'get_latest_by': 'id', + }, + ), + migrations.CreateModel( + name='BillContact', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), + ('name', models.CharField(verbose_name='name', blank=True, help_text='Account full name will be used when left blank.', max_length=256)), + ('address', models.TextField(verbose_name='address')), + ('city', models.CharField(verbose_name='city', default='Barcelona', max_length=128)), + ('zipcode', models.CharField(verbose_name='zip code', validators=[django.core.validators.RegexValidator('^[0-9A-Z]{3,10}$', 'Enter a valid zipcode.')], max_length=10)), + ('country', models.CharField(verbose_name='country', default='ES', choices=[('TR', 'Turkey'), ('BV', 'Bouvet Island'), ('EE', 'Estonia'), ('CO', 'Colombia'), ('MW', 'Malawi'), ('JM', 'Jamaica'), ('GF', 'French Guiana'), ('NR', 'Nauru'), ('DK', 'Denmark'), ('SY', 'Syrian Arab Republic'), ('PH', 'Philippines'), ('TF', 'French Southern Territories'), ('GH', 'Ghana'), ('AM', 'Armenia'), ('PY', 'Paraguay'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('EG', 'Egypt'), ('CU', 'Cuba'), ('VI', 'Virgin Islands (U.S.)'), ('KN', 'Saint Kitts and Nevis'), ('RU', 'Russian Federation'), ('RO', 'Romania'), ('MD', 'Moldova (the Republic of)'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('JP', 'Japan'), ('OM', 'Oman'), ('AE', 'United Arab Emirates'), ('BM', 'Bermuda'), ('VG', 'Virgin Islands (British)'), ('CD', 'Congo (the Democratic Republic of the)'), ('GY', 'Guyana'), ('IQ', 'Iraq'), ('DJ', 'Djibouti'), ('MU', 'Mauritius'), ('UG', 'Uganda'), ('ID', 'Indonesia'), ('KP', "Korea (the Democratic People's Republic of)"), ('CA', 'Canada'), ('MS', 'Montserrat'), ('SA', 'Saudi Arabia'), ('SZ', 'Swaziland'), ('NZ', 'New Zealand'), ('TO', 'Tonga'), ('IM', 'Isle of Man'), ('AZ', 'Azerbaijan'), ('PG', 'Papua New Guinea'), ('LB', 'Lebanon'), ('PR', 'Puerto Rico'), ('HM', 'Heard Island and McDonald Islands'), ('GR', 'Greece'), ('CR', 'Costa Rica'), ('PA', 'Panama'), ('BG', 'Bulgaria'), ('SS', 'South Sudan'), ('PE', 'Peru'), ('BY', 'Belarus'), ('FK', 'Falkland Islands [Malvinas]'), ('PF', 'French Polynesia'), ('MP', 'Northern Mariana Islands'), ('HN', 'Honduras'), ('SI', 'Slovenia'), ('GU', 'Guam'), ('PL', 'Poland'), ('CW', 'Curaçao'), ('BF', 'Burkina Faso'), ('PT', 'Portugal'), ('ZM', 'Zambia'), ('TZ', 'Tanzania, United Republic of'), ('WF', 'Wallis and Futuna'), ('DM', 'Dominica'), ('GT', 'Guatemala'), ('PS', 'Palestine, State of'), ('TN', 'Tunisia'), ('BE', 'Belgium'), ('SX', 'Sint Maarten (Dutch part)'), ('FJ', 'Fiji'), ('FO', 'Faroe Islands'), ('BH', 'Bahrain'), ('BL', 'Saint Barthélemy'), ('DE', 'Germany'), ('NU', 'Niue'), ('SV', 'El Salvador'), ('BS', 'Bahamas'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('SL', 'Sierra Leone'), ('SN', 'Senegal'), ('EH', 'Western Sahara'), ('TD', 'Chad'), ('NA', 'Namibia'), ('FI', 'Finland'), ('GW', 'Guinea-Bissau'), ('MT', 'Malta'), ('KY', 'Cayman Islands'), ('UM', 'United States Minor Outlying Islands'), ('LC', 'Saint Lucia'), ('GD', 'Grenada'), ('GM', 'Gambia'), ('HU', 'Hungary'), ('DZ', 'Algeria'), ('JO', 'Jordan'), ('ZW', 'Zimbabwe'), ('CY', 'Cyprus'), ('GL', 'Greenland'), ('UY', 'Uruguay'), ('MA', 'Morocco'), ('GP', 'Guadeloupe'), ('MY', 'Malaysia'), ('FR', 'France'), ('RE', 'Réunion'), ('MV', 'Maldives'), ('MN', 'Mongolia'), ('MO', 'Macao'), ('AU', 'Australia'), ('CX', 'Christmas Island'), ('VN', 'Viet Nam'), ('AS', 'American Samoa'), ('TK', 'Tokelau'), ('GS', 'South Georgia and the South Sandwich Islands'), ('KG', 'Kyrgyzstan'), ('AO', 'Angola'), ('TV', 'Tuvalu'), ('NI', 'Nicaragua'), ('QA', 'Qatar'), ('LT', 'Lithuania'), ('VA', 'Holy See'), ('PK', 'Pakistan'), ('GQ', 'Equatorial Guinea'), ('RS', 'Serbia'), ('KR', 'Korea (the Republic of)'), ('ER', 'Eritrea'), ('KW', 'Kuwait'), ('IR', 'Iran (Islamic Republic of)'), ('SK', 'Slovakia'), ('SE', 'Sweden'), ('TL', 'Timor-Leste'), ('AG', 'Antigua and Barbuda'), ('SD', 'Sudan'), ('BR', 'Brazil'), ('TM', 'Turkmenistan'), ('AI', 'Anguilla'), ('SR', 'Suriname'), ('MX', 'Mexico'), ('GE', 'Georgia'), ('KE', 'Kenya'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('VC', 'Saint Vincent and the Grenadines'), ('MF', 'Saint Martin (French part)'), ('CC', 'Cocos (Keeling) Islands'), ('GI', 'Gibraltar'), ('ME', 'Montenegro'), ('MC', 'Monaco'), ('ZA', 'South Africa'), ('IS', 'Iceland'), ('KM', 'Comoros'), ('KI', 'Kiribati'), ('HT', 'Haiti'), ('BO', 'Bolivia (Plurinational State of)'), ('CH', 'Switzerland'), ('MR', 'Mauritania'), ('GA', 'Gabon'), ('KZ', 'Kazakhstan'), ('BN', 'Brunei Darussalam'), ('YT', 'Mayotte'), ('IL', 'Israel'), ('YE', 'Yemen'), ('SO', 'Somalia'), ('TJ', 'Tajikistan'), ('CZ', 'Czech Republic'), ('SC', 'Seychelles'), ('RW', 'Rwanda'), ('SG', 'Singapore'), ('SB', 'Solomon Islands'), ('AX', 'Åland Islands'), ('PN', 'Pitcairn'), ('NF', 'Norfolk Island'), ('AR', 'Argentina'), ('BD', 'Bangladesh'), ('GN', 'Guinea'), ('AF', 'Afghanistan'), ('VU', 'Vanuatu'), ('NL', 'Netherlands'), ('LA', "Lao People's Democratic Republic"), ('BW', 'Botswana'), ('BA', 'Bosnia and Herzegovina'), ('ST', 'Sao Tome and Principe'), ('GG', 'Guernsey'), ('BJ', 'Benin'), ('IT', 'Italy'), ('EC', 'Ecuador'), ('LY', 'Libya'), ('FM', 'Micronesia (Federated States of)'), ('AW', 'Aruba'), ('MG', 'Madagascar'), ('UZ', 'Uzbekistan'), ('AD', 'Andorra'), ('HK', 'Hong Kong'), ('PW', 'Palau'), ('PM', 'Saint Pierre and Miquelon'), ('AT', 'Austria'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('ET', 'Ethiopia'), ('US', 'United States of America'), ('CV', 'Cabo Verde'), ('SJ', 'Svalbard and Jan Mayen'), ('IO', 'British Indian Ocean Territory'), ('BB', 'Barbados'), ('CK', 'Cook Islands'), ('NC', 'New Caledonia'), ('BI', 'Burundi'), ('TT', 'Trinidad and Tobago'), ('CG', 'Congo'), ('CM', 'Cameroon'), ('KH', 'Cambodia'), ('TG', 'Togo'), ('CL', 'Chile'), ('CF', 'Central African Republic'), ('IN', 'India'), ('NP', 'Nepal'), ('TC', 'Turks and Caicos Islands'), ('MM', 'Myanmar'), ('MQ', 'Martinique'), ('LI', 'Liechtenstein'), ('JE', 'Jersey'), ('SM', 'San Marino'), ('MZ', 'Mozambique'), ('UA', 'Ukraine'), ('LV', 'Latvia'), ('MH', 'Marshall Islands'), ('AL', 'Albania'), ('TW', 'Taiwan (Province of China)'), ('DO', 'Dominican Republic'), ('ES', 'Spain'), ('IE', 'Ireland'), ('WS', 'Samoa'), ('HR', 'Croatia'), ('AQ', 'Antarctica'), ('ML', 'Mali'), ('NE', 'Niger'), ('BZ', 'Belize'), ('TH', 'Thailand'), ('CN', 'China'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('NG', 'Nigeria'), ('LU', 'Luxembourg'), ('BT', 'Bhutan'), ('NO', 'Norway'), ('CI', "Côte d'Ivoire"), ('LS', 'Lesotho')], max_length=20)), + ('vat', models.CharField(verbose_name='VAT number', max_length=64)), + ('account', models.OneToOneField(verbose_name='account', related_name='billcontact', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='BillLine', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), + ('description', models.CharField(verbose_name='description', max_length=256)), + ('rate', models.DecimalField(verbose_name='rate', decimal_places=2, max_digits=12, null=True, blank=True)), + ('quantity', models.DecimalField(verbose_name='quantity', decimal_places=2, max_digits=12)), + ('verbose_quantity', models.CharField(verbose_name='Verbose quantity', max_length=16)), + ('subtotal', models.DecimalField(verbose_name='subtotal', decimal_places=2, max_digits=12)), + ('tax', models.DecimalField(verbose_name='tax', decimal_places=2, max_digits=2)), + ('order_billed_on', models.DateField(verbose_name='order billed', null=True, blank=True)), + ('order_billed_until', models.DateField(verbose_name='order billed until', null=True, blank=True)), + ('created_on', models.DateField(verbose_name='created', auto_now_add=True)), + ('amended_line', models.ForeignKey(verbose_name='amended line', blank=True, null=True, related_name='amendment_lines', to='bills.BillLine')), + ('bill', models.ForeignKey(verbose_name='bill', related_name='lines', to='bills.Bill')), + ('order', models.ForeignKey(help_text='Informative link back to the order', blank=True, on_delete=django.db.models.deletion.SET_NULL, to='orders.Order', null=True)), + ], + ), + migrations.CreateModel( + name='BillSubline', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), + ('description', models.CharField(verbose_name='description', max_length=256)), + ('total', models.DecimalField(decimal_places=2, max_digits=12)), + ('type', models.CharField(verbose_name='type', default='OTHER', choices=[('VOLUME', 'Volume'), ('COMPENSATION', 'Compensation'), ('OTHER', 'Other')], max_length=16)), + ('line', models.ForeignKey(verbose_name='bill line', related_name='sublines', to='bills.BillLine')), + ], + ), + migrations.CreateModel( + name='AmendmentFee', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('bills.bill',), + ), + migrations.CreateModel( + name='AmendmentInvoice', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('bills.bill',), + ), + migrations.CreateModel( + name='Fee', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('bills.bill',), + ), + migrations.CreateModel( + name='Invoice', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('bills.bill',), + ), + migrations.CreateModel( + name='ProForma', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('bills.bill',), + ), + ] diff --git a/orchestra/contrib/bills/migrations/0002_auto_20150413_1937.py b/orchestra/contrib/bills/migrations/0002_auto_20150413_1937.py new file mode 100644 index 00000000..30b23cbb --- /dev/null +++ b/orchestra/contrib/bills/migrations/0002_auto_20150413_1937.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bills', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='billcontact', + name='country', + field=models.CharField(choices=[('KZ', 'Kazakhstan'), ('IM', 'Isle of Man'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('PW', 'Palau'), ('WF', 'Wallis and Futuna'), ('HK', 'Hong Kong'), ('BO', 'Bolivia (Plurinational State of)'), ('RE', 'Réunion'), ('PS', 'Palestine, State of'), ('IE', 'Ireland'), ('CH', 'Switzerland'), ('AR', 'Argentina'), ('LA', "Lao People's Democratic Republic"), ('BA', 'Bosnia and Herzegovina'), ('IR', 'Iran (Islamic Republic of)'), ('BD', 'Bangladesh'), ('ER', 'Eritrea'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('SJ', 'Svalbard and Jan Mayen'), ('ZW', 'Zimbabwe'), ('IN', 'India'), ('TW', 'Taiwan (Province of China)'), ('DO', 'Dominican Republic'), ('PE', 'Peru'), ('HT', 'Haiti'), ('MO', 'Macao'), ('ST', 'Sao Tome and Principe'), ('VG', 'Virgin Islands (British)'), ('ME', 'Montenegro'), ('IT', 'Italy'), ('IQ', 'Iraq'), ('MT', 'Malta'), ('AG', 'Antigua and Barbuda'), ('UZ', 'Uzbekistan'), ('KN', 'Saint Kitts and Nevis'), ('TD', 'Chad'), ('AI', 'Anguilla'), ('MM', 'Myanmar'), ('AM', 'Armenia'), ('UY', 'Uruguay'), ('BB', 'Barbados'), ('BN', 'Brunei Darussalam'), ('CN', 'China'), ('AL', 'Albania'), ('AQ', 'Antarctica'), ('GT', 'Guatemala'), ('NR', 'Nauru'), ('UM', 'United States Minor Outlying Islands'), ('MP', 'Northern Mariana Islands'), ('SR', 'Suriname'), ('GY', 'Guyana'), ('LV', 'Latvia'), ('LS', 'Lesotho'), ('ES', 'Spain'), ('TC', 'Turks and Caicos Islands'), ('VA', 'Holy See'), ('NZ', 'New Zealand'), ('SK', 'Slovakia'), ('BE', 'Belgium'), ('TG', 'Togo'), ('SN', 'Senegal'), ('CG', 'Congo'), ('MN', 'Mongolia'), ('GA', 'Gabon'), ('GW', 'Guinea-Bissau'), ('HU', 'Hungary'), ('TR', 'Turkey'), ('GE', 'Georgia'), ('EH', 'Western Sahara'), ('PN', 'Pitcairn'), ('FJ', 'Fiji'), ('TV', 'Tuvalu'), ('AZ', 'Azerbaijan'), ('MZ', 'Mozambique'), ('GL', 'Greenland'), ('US', 'United States of America'), ('BF', 'Burkina Faso'), ('BT', 'Bhutan'), ('VN', 'Viet Nam'), ('PM', 'Saint Pierre and Miquelon'), ('PY', 'Paraguay'), ('FR', 'France'), ('DZ', 'Algeria'), ('LT', 'Lithuania'), ('NU', 'Niue'), ('MY', 'Malaysia'), ('DM', 'Dominica'), ('NC', 'New Caledonia'), ('NA', 'Namibia'), ('WS', 'Samoa'), ('MW', 'Malawi'), ('BW', 'Botswana'), ('SM', 'San Marino'), ('HM', 'Heard Island and McDonald Islands'), ('IS', 'Iceland'), ('CF', 'Central African Republic'), ('SB', 'Solomon Islands'), ('LK', 'Sri Lanka'), ('ID', 'Indonesia'), ('GR', 'Greece'), ('CO', 'Colombia'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('KR', 'Korea (the Republic of)'), ('SZ', 'Swaziland'), ('KE', 'Kenya'), ('AF', 'Afghanistan'), ('AE', 'United Arab Emirates'), ('DK', 'Denmark'), ('TZ', 'Tanzania, United Republic of'), ('AD', 'Andorra'), ('KH', 'Cambodia'), ('CY', 'Cyprus'), ('GS', 'South Georgia and the South Sandwich Islands'), ('EG', 'Egypt'), ('UG', 'Uganda'), ('TK', 'Tokelau'), ('MS', 'Montserrat'), ('YT', 'Mayotte'), ('MU', 'Mauritius'), ('BS', 'Bahamas'), ('CD', 'Congo (the Democratic Republic of the)'), ('CZ', 'Czech Republic'), ('CR', 'Costa Rica'), ('NL', 'Netherlands'), ('GQ', 'Equatorial Guinea'), ('SS', 'South Sudan'), ('RW', 'Rwanda'), ('VU', 'Vanuatu'), ('DE', 'Germany'), ('PL', 'Poland'), ('CX', 'Christmas Island'), ('AO', 'Angola'), ('BZ', 'Belize'), ('CK', 'Cook Islands'), ('TO', 'Tonga'), ('MA', 'Morocco'), ('CU', 'Cuba'), ('JM', 'Jamaica'), ('NI', 'Nicaragua'), ('AT', 'Austria'), ('FI', 'Finland'), ('FO', 'Faroe Islands'), ('VI', 'Virgin Islands (U.S.)'), ('BR', 'Brazil'), ('SY', 'Syrian Arab Republic'), ('ET', 'Ethiopia'), ('BJ', 'Benin'), ('PH', 'Philippines'), ('AS', 'American Samoa'), ('TL', 'Timor-Leste'), ('AU', 'Australia'), ('SX', 'Sint Maarten (Dutch part)'), ('PG', 'Papua New Guinea'), ('NG', 'Nigeria'), ('SL', 'Sierra Leone'), ('LB', 'Lebanon'), ('OM', 'Oman'), ('SG', 'Singapore'), ('CM', 'Cameroon'), ('PT', 'Portugal'), ('KM', 'Comoros'), ('IO', 'British Indian Ocean Territory'), ('BM', 'Bermuda'), ('YE', 'Yemen'), ('RU', 'Russian Federation'), ('GM', 'Gambia'), ('SI', 'Slovenia'), ('GI', 'Gibraltar'), ('LY', 'Libya'), ('GU', 'Guam'), ('LU', 'Luxembourg'), ('RO', 'Romania'), ('MD', 'Moldova (the Republic of)'), ('BL', 'Saint Barthélemy'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('EE', 'Estonia'), ('LC', 'Saint Lucia'), ('TJ', 'Tajikistan'), ('IL', 'Israel'), ('PA', 'Panama'), ('PR', 'Puerto Rico'), ('CL', 'Chile'), ('KP', "Korea (the Democratic People's Republic of)"), ('GF', 'French Guiana'), ('CI', "Côte d'Ivoire"), ('MR', 'Mauritania'), ('NF', 'Norfolk Island'), ('BG', 'Bulgaria'), ('SD', 'Sudan'), ('NO', 'Norway'), ('GG', 'Guernsey'), ('CV', 'Cabo Verde'), ('GP', 'Guadeloupe'), ('BV', 'Bouvet Island'), ('TT', 'Trinidad and Tobago'), ('SO', 'Somalia'), ('DJ', 'Djibouti'), ('MC', 'Monaco'), ('JP', 'Japan'), ('NP', 'Nepal'), ('JE', 'Jersey'), ('TN', 'Tunisia'), ('RS', 'Serbia'), ('NE', 'Niger'), ('ML', 'Mali'), ('KW', 'Kuwait'), ('PF', 'French Polynesia'), ('ZA', 'South Africa'), ('GH', 'Ghana'), ('BH', 'Bahrain'), ('CC', 'Cocos (Keeling) Islands'), ('MQ', 'Martinique'), ('AW', 'Aruba'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('JO', 'Jordan'), ('LI', 'Liechtenstein'), ('TM', 'Turkmenistan'), ('MX', 'Mexico'), ('LR', 'Liberia'), ('GD', 'Grenada'), ('HN', 'Honduras'), ('SA', 'Saudi Arabia'), ('PK', 'Pakistan'), ('CA', 'Canada'), ('CW', 'Curaçao'), ('SV', 'El Salvador'), ('SC', 'Seychelles'), ('QA', 'Qatar'), ('EC', 'Ecuador'), ('VC', 'Saint Vincent and the Grenadines'), ('HR', 'Croatia'), ('MV', 'Maldives'), ('KI', 'Kiribati'), ('FM', 'Micronesia (Federated States of)'), ('BY', 'Belarus'), ('FK', 'Falkland Islands [Malvinas]'), ('TF', 'French Southern Territories'), ('AX', 'Åland Islands'), ('MG', 'Madagascar'), ('SE', 'Sweden'), ('BI', 'Burundi'), ('KY', 'Cayman Islands'), ('MF', 'Saint Martin (French part)'), ('ZM', 'Zambia'), ('GN', 'Guinea'), ('UA', 'Ukraine'), ('TH', 'Thailand'), ('KG', 'Kyrgyzstan'), ('MH', 'Marshall Islands')], verbose_name='country', max_length=20, default='ES'), + ), + migrations.AlterField( + model_name='billline', + name='tax', + field=models.DecimalField(decimal_places=2, verbose_name='tax', max_digits=4), + ), + ] diff --git a/orchestra/contrib/bills/migrations/__init__.py b/orchestra/contrib/bills/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index e46778ec..6a25a270 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -2,6 +2,8 @@ from dateutil.relativedelta import relativedelta from django.core.validators import ValidationError, RegexValidator from django.db import models +from django.db.models import F, Sum +from django.db.models.functions import Coalesce from django.template import loader, Context from django.utils import timezone, translation from django.utils.encoding import force_text @@ -89,6 +91,7 @@ class Bill(models.Model): is_sent = models.BooleanField(_("sent"), default=False) due_on = models.DateField(_("due on"), null=True, blank=True) updated_on = models.DateField(_("updated on"), auto_now=True) + # TODO allways compute total or what? total = models.DecimalField(max_digits=12, decimal_places=2, default=0) comments = models.TextField(_("comments"), blank=True) html = models.TextField(_("HTML"), blank=True) @@ -227,18 +230,17 @@ class Bill(models.Model): def get_subtotals(self): subtotals = {} - for line in self.lines.all(): - subtotal, taxes = subtotals.get(line.tax, (0, 0)) - subtotal += line.get_total() - subtotals[line.tax] = (subtotal, (line.tax/100)*subtotal) + lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0))) + for tax, total in lines.values_list('tax', 'totals'): + subtotal, taxes = subtotals.get(tax, (0, 0)) + subtotal += total + subtotals[tax] = (subtotal, round(tax/100*subtotal, 2)) return subtotals def get_total(self): - total = 0 - for tax, subtotal in self.get_subtotals().items(): - subtotal, taxes = subtotal - total += subtotal + taxes - return total + totals = self.lines.annotate( + totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100)) + return round(totals.aggregate(Sum('totals'))['totals__sum'], 2) class Invoice(Bill): @@ -272,12 +274,12 @@ class BillLine(models.Model): description = models.CharField(_("description"), max_length=256) rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2) quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2) + verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16) subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2) - tax = models.DecimalField(_("tax"), max_digits=2, decimal_places=2) + tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2) # Undo # initial = models.DateTimeField(null=True) # end = models.DateTimeField(null=True) - order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True, help_text=_("Informative link back to the order"), on_delete=models.SET_NULL) order_billed_on = models.DateField(_("order billed"), null=True, blank=True) @@ -297,10 +299,11 @@ class BillLine(models.Model): def get_total(self): """ Computes subline discounts """ - total = self.subtotal - for subline in self.sublines.all(): - total += subline.total - return total + if self.pk: + return self.subtotal + sum(self.sublines.values_list('total', flat=True)) + + def get_verbose_quantity(self): + return self.verbose_quantity or self.quantity def undo(self): # TODO warn user that undoing bills with compensations lead to compensation lost @@ -313,12 +316,11 @@ class BillLine(models.Model): self.order.billed_on = self.order_billed_on self.delete() - def save(self, *args, **kwargs): - # TODO cost and consistency of this shit - super(BillLine, self).save(*args, **kwargs) - if self.bill.is_open: - self.bill.total = self.bill.get_total() - self.bill.save(update_fields=['total']) +# def save(self, *args, **kwargs): +# super(BillLine, self).save(*args, **kwargs) +# if self.bill.is_open: +# self.bill.total = self.bill.get_total() +# self.bill.save(update_fields=['total']) class BillSubline(models.Model): @@ -339,12 +341,12 @@ class BillSubline(models.Model): total = models.DecimalField(max_digits=12, decimal_places=2) type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER) - def save(self, *args, **kwargs): - # TODO cost of this shit - super(BillSubline, self).save(*args, **kwargs) - if self.line.bill.is_open: - self.line.bill.total = self.line.bill.get_total() - self.line.bill.save(update_fields=['total']) +# def save(self, *args, **kwargs): +# # TODO cost of this shit +# super(BillSubline, self).save(*args, **kwargs) +# if self.line.bill.is_open: +# self.line.bill.total = self.line.bill.get_total() +# self.line.bill.save(update_fields=['total']) accounts.register(Bill) diff --git a/orchestra/contrib/bills/templates/bills/microspective.html b/orchestra/contrib/bills/templates/bills/microspective.html index cd555390..be997133 100644 --- a/orchestra/contrib/bills/templates/bills/microspective.html +++ b/orchestra/contrib/bills/templates/bills/microspective.html @@ -79,7 +79,7 @@ {% with sublines=line.sublines.all %} {{ line.id }} {{ line.description }} - {{ line.quantity|default:" " }} + {{ line.get_verbose_quantity|default:" "|safe }} {% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %} {{ line.subtotal }} &{{ currency.lower }};
@@ -96,7 +96,7 @@

 
- {% for tax, subtotal in bill.get_subtotals.iteritems %} + {% for tax, subtotal in bill.get_subtotals.items %} subtotal {{ tax }}% {% trans "VAT" %} {{ subtotal | first }} &{{ currency.lower }};
diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py index 62bf5f2d..955f8c3d 100644 --- a/orchestra/contrib/orchestration/admin.py +++ b/orchestra/contrib/orchestration/admin.py @@ -37,7 +37,7 @@ class RouteAdmin(admin.ModelAdmin): for backend, __ in ServiceBackend.get_choices() } DEFAULT_MATCH = { - backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends(active=False) + backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends() } def display_model(self, route): diff --git a/orchestra/contrib/orchestration/backends.py b/orchestra/contrib/orchestration/backends.py index 1867f5c5..d0e9e598 100644 --- a/orchestra/contrib/orchestration/backends.py +++ b/orchestra/contrib/orchestration/backends.py @@ -99,15 +99,12 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount): return None @classmethod - def get_backends(cls, instance=None, action=None, active=True): - from .models import Route + def get_backends(cls, instance=None, action=None, active=None): backends = cls.get_plugins() included = [] - if active: - active_backends = Route.objects.filter(is_active=True).values_list('backend', flat=True) # Filter for instance or action for backend in backends: - if active and backend.get_name() not in active_backends: + if active is not None and backend.get_name() not in active: continue include = True if instance: @@ -208,5 +205,5 @@ class ServiceController(ServiceBackend): """ filter controller classes """ backends = super(ServiceController, cls).get_backends() return [ - backend for backend in backends if ServiceController in backend.__mro__ + backend for backend in backends if isinstance(backend, ServiceController) ] diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py index 991e823b..a2a06724 100644 --- a/orchestra/contrib/orchestration/manager.py +++ b/orchestra/contrib/orchestration/manager.py @@ -141,7 +141,8 @@ def collect(instance, action, **kwargs): """ collect operations """ operations = kwargs.get('operations', set()) route_cache = kwargs.get('route_cache', {}) - for backend_cls in ServiceBackend.get_backends(): + active_backends = kwargs.get('active_backends', None) + for backend_cls in ServiceBackend.get_backends(active=active_backends): # Check if there exists a related instance to be executed for this backend and action instances = [] if action in backend_cls.actions: diff --git a/orchestra/contrib/orchestration/middlewares.py b/orchestra/contrib/orchestration/middlewares.py index 3b1a96d2..c1ece185 100644 --- a/orchestra/contrib/orchestration/middlewares.py +++ b/orchestra/contrib/orchestration/middlewares.py @@ -6,13 +6,16 @@ from django.db.models.signals import pre_delete, post_save, m2m_changed from django.dispatch import receiver from django.http.response import HttpResponseServerError -from orchestra.utils.python import OrderedSet +from orchestra.utils.python import OrderedSet, import_class -from . import manager, Operation +from . import manager, Operation, settings from .helpers import message_user from .models import BackendLog +router = import_class(settings.ORCHESTRATION_ROUTER) + + @receiver(post_save, dispatch_uid='orchestration.post_save_collector') def post_save_collector(sender, *args, **kwargs): if sender not in [BackendLog, Operation]: @@ -63,6 +66,16 @@ class OperationsMiddleware(object): return request.route_cache return {} + @classmethod + def get_active_cache(cls): + """ chache the routes to save sql queries """ + if hasattr(cls.thread_locals, 'request'): + request = cls.thread_locals.request + if not hasattr(request, 'active_cache'): + request.active_cache = router.get_active_backends() + return request.active_cache + return router.get_active_backends() + @classmethod def collect(cls, action, **kwargs): """ Collects all pending operations derived from model signals """ @@ -71,6 +84,7 @@ class OperationsMiddleware(object): return kwargs['operations'] = cls.get_pending_operations() kwargs['route_cache'] = cls.get_route_cache() + kwargs['active_backends'] = cls.get_active_cache() instance = kwargs.pop('instance') manager.collect(instance, action, **kwargs) diff --git a/orchestra/contrib/orchestration/models.py b/orchestra/contrib/orchestration/models.py index 14012d96..ef731fe0 100644 --- a/orchestra/contrib/orchestration/models.py +++ b/orchestra/contrib/orchestration/models.py @@ -169,6 +169,10 @@ class Route(models.Model): servers.append(route.host) return servers + @classmethod + def get_active_backends(cls): + return cls.objects.filter(is_active=True).values_list('backend', flat=True) + def clean(self): if not self.match: self.match = 'True' diff --git a/orchestra/contrib/orders/billing.py b/orchestra/contrib/orders/billing.py index 45cc9b4e..d6606444 100644 --- a/orchestra/contrib/orders/billing.py +++ b/orchestra/contrib/orders/billing.py @@ -33,18 +33,20 @@ class BillsBackend(object): bill = Invoice.objects.create(account=account, is_open=True) bills.append(bill) # Create bill line - billine = bill.lines.create( + quantity = line.metric*line.size + if quantity != 0: + billine = bill.lines.create( rate=service.nominal_price, quantity=line.metric*line.size, + verbose_quantity=self.get_verbose_quantity(line), subtotal=line.subtotal, tax=service.tax, description=self.get_line_description(line), - order=line.order, order_billed_on=line.order.old_billed_on, order_billed_until=line.order.old_billed_until - ) - self.create_sublines(billine, line.discounts) + ) + self.create_sublines(billine, line.discounts) return bills def format_period(self, ini, end): @@ -61,12 +63,24 @@ class BillsBackend(object): description = line.order.description if service.billing_period != service.NEVER: description += " %s" % self.format_period(line.ini, line.end) - if service.metric and service.billing_period != service.NEVER and service.pricing_period == service.NEVER: - metric = format(line.metric, '.2f').rstrip('0').rstrip('.') - size = format(line.size, '.2f').rstrip('0').rstrip('.') - description += " (%sx%s)" % (metric, size) return description + def get_verbose_quantity(self, line): +# service = line.order.service +# if service.metric and service.billing_period != service.NEVER and service.pricing_period == service.NEVER: + metric = format(line.metric, '.2f').rstrip('0').rstrip('.') + if metric.endswith('.00'): + metric = metric.split('.')[0] + size = format(line.size, '.2f').rstrip('0').rstrip('.') + if size.endswith('.00'): + size = metric.split('.')[0] + if metric == '1': + return size + if size == '1': + return metric + return "%s×%s" % (metric, size) +# return '' + def create_sublines(self, line, discounts): for discount in discounts: line.sublines.create( diff --git a/orchestra/contrib/resources/aggregations.py b/orchestra/contrib/resources/aggregations.py index be36e01d..3ecba134 100644 --- a/orchestra/contrib/resources/aggregations.py +++ b/orchestra/contrib/resources/aggregations.py @@ -30,8 +30,6 @@ class Last(Aggregation): return dataset.none() def compute_usage(self, dataset): - # FIXME Aggregation of 0s returns None! django bug? - # value = dataset.aggregate(models.Sum('value'))['value__sum'] values = dataset.values_list('value', flat=True) if values: return sum(values) diff --git a/orchestra/contrib/resources/backends.py b/orchestra/contrib/resources/backends.py index db8c4325..37257a19 100644 --- a/orchestra/contrib/resources/backends.py +++ b/orchestra/contrib/resources/backends.py @@ -21,7 +21,7 @@ class ServiceMonitor(ServiceBackend): def get_plugins(cls): """ filter controller classes """ return [ - plugin for plugin in cls.plugins if ServiceMonitor in plugin.__mro__ + plugin for plugin in cls.plugins if isinstance(plugin, ServiceMonitor) ] @classmethod diff --git a/orchestra/contrib/services/handlers.py b/orchestra/contrib/services/handlers.py index bb512a1f..a5390914 100644 --- a/orchestra/contrib/services/handlers.py +++ b/orchestra/contrib/services/handlers.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from orchestra import plugins from orchestra.utils.humanize import text2int -from orchestra.utils.python import AttrDict +from orchestra.utils.python import AttrDict, cmp_to_key from . import settings, helpers @@ -399,6 +399,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): pend = order.billed_until or order.registered_on pini = pend - rdelta metric = self.get_register_or_renew_events(porders, pini, pend) + position = min(position, metric) price = self.get_price(account, metric, position=position, rates=rates) ini = order.billed_until or order.registered_on end = order.new_billed_until @@ -445,8 +446,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE: # Get orders pending for compensation givers = list(related_orders.givers(ini, end)) - givers.sort(cmp=helpers.cmp_billed_until_or_registered_on) - orders.sort(cmp=helpers.cmp_billed_until_or_registered_on) + givers = sorted(givers, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on)) + orders = sorted(orders, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on)) self.assign_compensations(givers, orders, **options) rates = self.get_rates(account) @@ -459,7 +460,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): ini -= rdelta porders = related_orders.pricing_orders(ini, end) porders = list(set(orders).union(set(porders))) - porders.sort(cmp=helpers.cmp_billed_until_or_registered_on) + porders = sorted(porders, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on)) if concurrent: # Periodic billing with no pricing period lines = self.bill_concurrent_orders(account, porders, rates, ini, end) diff --git a/orchestra/contrib/services/models.py b/orchestra/contrib/services/models.py index a816d9d7..aaa9a0c5 100644 --- a/orchestra/contrib/services/models.py +++ b/orchestra/contrib/services/models.py @@ -214,11 +214,13 @@ class Service(models.Model): ant_counter = counter accumulated += rate['price'] * rate['quantity'] else: + if metric < position: + raise ValueError("Metric can not be less than the position.") for rate in rates: counter += rate['quantity'] if counter >= position: return decimal.Decimal(str(rate['price'])) - + def get_rates(self, account, cache=True): # rates are cached per account if not cache: diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py index 9b0c5234..fe86731d 100644 --- a/orchestra/contrib/webapps/backends/php.py +++ b/orchestra/contrib/webapps/backends/php.py @@ -118,12 +118,11 @@ class PHPBackend(WebAppServiceMixin, ServiceController): super(PHPBackend, self).commit() def get_fpm_config(self, webapp, context): - merge = settings.WEBAPPS_MERGE_PHP_WEBAPPS + options = webapp.get_options(merge=self.MERGE) context.update({ 'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE), - 'max_children': webapp.get_options().get('processes', - settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN), - 'request_terminate_timeout': webapp.get_options().get('timeout', False), + 'max_children': options.get('processes', settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN), + 'request_terminate_timeout': options.get('timeout', False), }) context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context fpm_config = Template(textwrap.dedent("""\ @@ -139,7 +138,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController): pm.max_requests = {{ max_requests }} pm.max_children = {{ max_children }} {% if request_terminate_timeout %}request_terminate_timeout = {{ request_terminate_timeout }}{% endif %} - {% for name, value in init_vars.iteritems %} + {% for name, value in init_vars.items %} php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %} """ )) @@ -168,9 +167,10 @@ class PHPBackend(WebAppServiceMixin, ServiceController): exec %(php_binary_path)s %(php_init_vars)s""") % context def get_fcgid_cmd_options(self, webapp, context): + options = webapp.get_options(merge=self.MERGE) maps = { - 'MaxProcesses': webapp.get_options().get('processes', None), - 'IOTimeout': webapp.get_options().get('timeout', None), + 'MaxProcesses': options.get('processes', None), + 'IOTimeout': options.get('timeout', None), } cmd_options = [] for directive, value in maps.items(): diff --git a/orchestra/contrib/webapps/backends/wordpress.py b/orchestra/contrib/webapps/backends/wordpress.py index 6347db18..4ada7fec 100644 --- a/orchestra/contrib/webapps/backends/wordpress.py +++ b/orchestra/contrib/webapps/backends/wordpress.py @@ -39,7 +39,6 @@ class WordPressBackend(WebAppServiceMixin, ServiceController): exc('wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate | tar -xzvf - -C %(app_path)s --strip-components=1'); exc('mkdir %(app_path)s/wp-content/uploads'); exc('chmod 750 %(app_path)s/wp-content/uploads'); - exc('chown -R %(user)s:%(group)s %(app_path)s'); $config_file = file('%(app_path)s/' . 'wp-config-sample.php'); $secret_keys = file_get_contents('https://api.wordpress.org/secret-key/1.1/salt/'); @@ -70,6 +69,8 @@ class WordPressBackend(WebAppServiceMixin, ServiceController): foreach ( $config_file as $line_num => $line ) { fwrite($fw, $line); } + exc('chown -R %(user)s:%(group)s %(app_path)s'); + define('WP_CONTENT_DIR', 'wp-content/'); define('WP_LANG_DIR', WP_CONTENT_DIR . '/languages' ); define('WP_USE_THEMES', true); diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py index 90fcf216..bb132e20 100644 --- a/orchestra/contrib/webapps/models.py +++ b/orchestra/contrib/webapps/models.py @@ -56,8 +56,17 @@ class WebApp(models.Model): self.data = apptype.clean_data() @cached - def get_options(self): - return OrderedDict((opt.name, opt.value) for opt in self.options.all().order_by('name')) + def get_options(self, merge=False): + if merge: + options = OrderedDict() + qs = WebAppOption.objects.filter(webapp__account=self.account, webapp__type=self.type) + for name, value in qs.values_list('name', 'value').order_by('name'): + if name in options: + options[name] = max(options[name], value) + else: + options[name] = value + return options + return OrderedDict(self.options.values_list('name', 'value').order_by('name')) def get_directive(self): return self.type_instance.get_directive() diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py index c4cd4f2d..a2fd8d3b 100644 --- a/orchestra/contrib/webapps/settings.py +++ b/orchestra/contrib/webapps/settings.py @@ -24,6 +24,7 @@ WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH', WEBAPPS_FCGID_WRAPPER_PATH = getattr(settings, 'WEBAPPS_FCGID_WRAPPER_PATH', # Inside SuExec Document root + # Make sure all account wrappers are in the same DIR '/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper' ) diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index 6519cd82..4ad78ee6 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -192,10 +192,11 @@ class Apache2Backend(ServiceController): 'wrapper_name': os.path.basename(wrapper_path), }) directives = '' - # This Alias trick is used instead of FcgidWrapper because we don't want to define + # This Action trick is used instead of FcgidWrapper because we don't want to define # a new fcgid process class each time an app is mounted (num proc limits enforcement). if 'wrapper_dir' not in context: # fcgi-bin only needs to be defined once per vhots + # We assume that all account wrapper paths will share the same dir context['wrapper_dir'] = os.path.dirname(wrapper_path) directives = textwrap.dedent("""\ Alias /fcgi-bin/ %(wrapper_dir)s/ diff --git a/orchestra/utils/python.py b/orchestra/utils/python.py index 95f18e9d..8137088f 100644 --- a/orchestra/utils/python.py +++ b/orchestra/utils/python.py @@ -89,3 +89,24 @@ class CaptureStdout(list): def __exit__(self, *args): self.extend(self._stringio.getvalue().splitlines()) sys.stdout = self._stdout + + +def cmp_to_key(mycmp): + 'Convert a cmp= function into a key= function' + class K(object): + def __init__(self, obj, *args): + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + return K +