Compare commits

...

No commits in common. "main" and "master_old" have entirely different histories.

361 changed files with 1006 additions and 62598 deletions

View File

@ -0,0 +1,48 @@
# Generated by Django 2.2.28 on 2023-07-22 08:01
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.core.validators
class Migration(migrations.Migration):
initial = True
dependencies = [
('orchestration', '__first__'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='SystemUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[orchestra.core.validators.validate_username], verbose_name='username')),
('password', models.CharField(max_length=128, verbose_name='password')),
('home', models.CharField(blank=True, help_text='Starting location when login with this no-shell user.', max_length=256, verbose_name='home')),
('directory', models.CharField(blank=True, help_text="Optional directory relative to user's home.", max_length=256, verbose_name='directory')),
('shell', models.CharField(choices=[('/dev/null', 'No shell, FTP only'), ('/bin/rssh', 'No shell, SFTP/RSYNC only'), ('/usr/bin/git-shell', 'No shell, GIT only'), ('/bin/bash', '/bin/bash')], default='/dev/null', max_length=32, verbose_name='shell')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='systemusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('groups', models.ManyToManyField(blank=True, help_text='A new group will be created for the user. Which additional groups would you like them to be a member of?', to='systemusers.SystemUser')),
],
),
migrations.CreateModel(
name='WebappUsers',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[orchestra.core.validators.validate_username], verbose_name='username')),
('password', models.CharField(max_length=128, verbose_name='password')),
('home', models.CharField(blank=True, help_text='Starting location when login with this no-shell user.', max_length=256, verbose_name='home')),
('shell', models.CharField(choices=[('/dev/null', 'No shell, FTP only'), ('/bin/rssh', 'No shell, SFTP/RSYNC only'), ('/usr/bin/git-shell', 'No shell, GIT only'), ('/bin/bash', '/bin/bash')], default='/dev/null', max_length=32, verbose_name='shell')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('target_server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='orchestration.Server', verbose_name='Server')),
],
options={
'unique_together': {('username', 'target_server')},
},
),
]

View File

@ -0,0 +1,43 @@
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from orchestra.core import validators
from orchestra.contrib.systemusers.models import WebappUsers
from .models import WebApp
class WebappCreationForm(forms.ModelForm):
username = forms.CharField(label=_("Username"), max_length=16,
required=False, validators=[validators.validate_name],
help_text=_("Required. 16 characters or fewer. Letters, digits and "
"@/./+/-/_ only."),
error_messages={
'invalid': _("This value may contain 16 characters or fewer, only letters, numbers and "
"@/./+/-/_ characters.")})
user = forms.ModelChoiceField(required=False, queryset=WebappUsers.objects)
password1 = forms.CharField(label=_("Password"), required=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
validators=[validators.validate_password])
password2 = forms.CharField(label=_("Password confirmation"), required=False,
widget=forms.PasswordInput,
help_text=_("Enter the same password as above, for verification."))
class Meta:
model = WebApp
fields = ('username', 'account', 'type')
def __init__(self, *args, **kwargs):
super(WebappCreationForm, self).__init__(*args, **kwargs)
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
msg = _("The two password fields didn't match.")
raise ValidationError(msg)
return password2

View File

@ -0,0 +1,64 @@
# Generated by Django 2.2.28 on 2023-08-17 09:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.core.validators
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('orchestration', '__first__'),
('domains', '__first__'),
('webapps', '0004_auto_20230817_1108'),
]
operations = [
migrations.CreateModel(
name='Content',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.CharField(blank=True, max_length=256, validators=[orchestra.core.validators.validate_url_path], verbose_name='path')),
('webapp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='webapps.WebApp', verbose_name='web application')),
],
),
migrations.CreateModel(
name='Website',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
('protocol', models.CharField(choices=[('http', 'HTTP'), ('https', 'HTTPS'), ('http/https', 'HTTP and HTTPS'), ('https-only', 'HTTPS only')], default='http', help_text='Select the protocol(s) for this website<br><tt>HTTPS only</tt> performs a redirection from <tt>http</tt> to <tt>https</tt>.', max_length=16, verbose_name='protocol')),
('is_active', models.BooleanField(default=True, verbose_name='active')),
('comments', models.TextField(blank=True, default='')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='websites', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('contents', models.ManyToManyField(through='websites.Content', to='webapps.WebApp')),
('domains', models.ManyToManyField(blank=True, related_name='websites', to='domains.Domain', verbose_name='domains')),
('target_server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='websites', to='orchestration.Server', verbose_name='Target Server')),
],
options={
'unique_together': {('name', 'account', 'target_server')},
},
),
migrations.CreateModel(
name='WebsiteDirective',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(choices=[(None, '-------'), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')]), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')])], db_index=True, max_length=128, verbose_name='name')),
('value', models.CharField(blank=True, max_length=256, verbose_name='value')),
('website', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='directives', to='websites.Website', verbose_name='web site')),
],
),
migrations.AddField(
model_name='content',
name='website',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='websites.Website', verbose_name='web site'),
),
migrations.AlterUniqueTogether(
name='content',
unique_together={('website', 'path')},
),
]

View File

@ -0,0 +1,64 @@
# Generated by Django 2.2.28 on 2023-08-17 09:42
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.core.validators
class Migration(migrations.Migration):
initial = True
dependencies = [
('orchestration', '__first__'),
('domains', '__first__'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('webapps', '0004_auto_20230817_1108'),
]
operations = [
migrations.CreateModel(
name='Content',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.CharField(blank=True, max_length=256, validators=[orchestra.core.validators.validate_url_path], verbose_name='path')),
('webapp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='webapps.WebApp', verbose_name='web application')),
],
),
migrations.CreateModel(
name='Website',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
('protocol', models.CharField(choices=[('http', 'HTTP'), ('https', 'HTTPS'), ('http/https', 'HTTP and HTTPS'), ('https-only', 'HTTPS only')], default='http', help_text='Select the protocol(s) for this website<br><tt>HTTPS only</tt> performs a redirection from <tt>http</tt> to <tt>https</tt>.', max_length=16, verbose_name='protocol')),
('is_active', models.BooleanField(default=True, verbose_name='active')),
('comments', models.TextField(blank=True, default='')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='websites', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('contents', models.ManyToManyField(through='websites.Content', to='webapps.WebApp')),
('domains', models.ManyToManyField(blank=True, related_name='websites', to='domains.Domain', verbose_name='domains')),
('target_server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='websites', to='orchestration.Server', verbose_name='Target Server')),
],
options={
'unique_together': {('name', 'account')},
},
),
migrations.CreateModel(
name='WebsiteDirective',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(choices=[(None, '-------'), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')]), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')])], db_index=True, max_length=128, verbose_name='name')),
('value', models.CharField(blank=True, max_length=256, verbose_name='value')),
('website', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='directives', to='websites.Website', verbose_name='web site')),
],
),
migrations.AddField(
model_name='content',
name='website',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='websites.Website', verbose_name='web site'),
),
migrations.AlterUniqueTogether(
name='content',
unique_together={('website', 'path')},
),
]

View File

@ -0,0 +1,64 @@
# Generated by Django 2.2.28 on 2023-08-17 09:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.core.validators
class Migration(migrations.Migration):
initial = True
dependencies = [
('domains', '__first__'),
('orchestration', '__first__'),
('webapps', '0004_auto_20230817_1108'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Content',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.CharField(blank=True, max_length=256, validators=[orchestra.core.validators.validate_url_path], verbose_name='path')),
('webapp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='webapps.WebApp', verbose_name='web application')),
],
),
migrations.CreateModel(
name='Website',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
('protocol', models.CharField(choices=[('http', 'HTTP'), ('https', 'HTTPS'), ('http/https', 'HTTP and HTTPS'), ('https-only', 'HTTPS only')], default='http', help_text='Select the protocol(s) for this website<br><tt>HTTPS only</tt> performs a redirection from <tt>http</tt> to <tt>https</tt>.', max_length=16, verbose_name='protocol')),
('is_active', models.BooleanField(default=True, verbose_name='active')),
('comments', models.TextField(blank=True, default='')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='websites', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('contents', models.ManyToManyField(through='websites.Content', to='webapps.WebApp')),
('domains', models.ManyToManyField(blank=True, related_name='websites', to='domains.Domain', verbose_name='domains')),
('target_server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='websites', to='orchestration.Server', verbose_name='Target Server')),
],
options={
'unique_together': {('name', 'account', 'target_server')},
},
),
migrations.CreateModel(
name='WebsiteDirective',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(choices=[(None, '-------'), ('HTTPD', [('redirect', 'Redirection'), ('proxy', 'Proxy'), ('error-document', 'ErrorDocumentRoot')]), ('SSL', [('ssl-ca', 'SSL CA'), ('ssl-cert', 'SSL cert'), ('ssl-key', 'SSL key')]), ('ModSecurity', [('sec-rule-remove', 'SecRuleRemoveById'), ('sec-engine', 'SecRuleEngine Off')]), ('SaaS', [('wordpress-saas', 'WordPress SaaS'), ('dokuwiki-saas', 'DokuWiki SaaS'), ('drupal-saas', 'Drupdal SaaS'), ('moodle-saas', 'Moodle SaaS')])], db_index=True, max_length=128, verbose_name='name')),
('value', models.CharField(blank=True, max_length=256, verbose_name='value')),
('website', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='directives', to='websites.Website', verbose_name='web site')),
],
),
migrations.AddField(
model_name='content',
name='website',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='websites.Website', verbose_name='web site'),
),
migrations.AlterUniqueTogether(
name='content',
unique_together={('website', 'path')},
),
]

View File

@ -0,0 +1,3 @@
[Trash Info]
Path=orchestra/contrib/systemusers/migrations/0001_initial.py
DeletionDate=2023-07-22T10:04:42

View File

@ -0,0 +1,3 @@
[Trash Info]
Path=orchestra/contrib/webapps/forms.py
DeletionDate=2023-07-24T18:44:25

View File

@ -0,0 +1,3 @@
[Trash Info]
Path=orchestra/contrib/websites/migrations
DeletionDate=2023-08-17T11:42:31

View File

@ -0,0 +1,3 @@
[Trash Info]
Path=orchestra/contrib/websites/migrations
DeletionDate=2023-08-17T11:45:11

View File

@ -0,0 +1,3 @@
[Trash Info]
Path=orchestra/contrib/websites/migrations
DeletionDate=2023-08-17T11:14:47

View File

@ -0,0 +1,42 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="24">
<item index="0" class="java.lang.String" itemvalue="paramiko" />
<item index="1" class="java.lang.String" itemvalue="fabric" />
<item index="2" class="java.lang.String" itemvalue="django-admin-tools" />
<item index="3" class="java.lang.String" itemvalue="django-fluent-dashboard" />
<item index="4" class="java.lang.String" itemvalue="phonenumbers" />
<item index="5" class="java.lang.String" itemvalue="jsonfield" />
<item index="6" class="java.lang.String" itemvalue="amqp" />
<item index="7" class="java.lang.String" itemvalue="python-dateutil" />
<item index="8" class="java.lang.String" itemvalue="django-localflavor" />
<item index="9" class="java.lang.String" itemvalue="ecdsa" />
<item index="10" class="java.lang.String" itemvalue="kombu" />
<item index="11" class="java.lang.String" itemvalue="django-extensions" />
<item index="12" class="java.lang.String" itemvalue="celery" />
<item index="13" class="java.lang.String" itemvalue="django-countries" />
<item index="14" class="java.lang.String" itemvalue="Pygments" />
<item index="15" class="java.lang.String" itemvalue="django-filter" />
<item index="16" class="java.lang.String" itemvalue="Django" />
<item index="17" class="java.lang.String" itemvalue="anyjson" />
<item index="18" class="java.lang.String" itemvalue="django-celery" />
<item index="19" class="java.lang.String" itemvalue="djangorestframework" />
<item index="20" class="java.lang.String" itemvalue="Markdown" />
<item index="21" class="java.lang.String" itemvalue="django-iban" />
<item index="22" class="java.lang.String" itemvalue="billiard" />
<item index="23" class="java.lang.String" itemvalue="passlib" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

7
.idea/misc.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (zfs-blues)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/orchestra.iml" filepath="$PROJECT_DIR$/.idea/orchestra.iml" />
</modules>
</component>
</project>

32
.idea/orchestra.iml Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/orchestra/conf/project_template" />
<option name="settingsModule" value="orchestra/settings.py" />
<option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/orchestra/contrib/bills/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="projectConfiguration" value="pytest" />
<option name="PROJECT_TEST_RUNNER" value="pytest" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

261
.idea/workspace.xml Normal file
View File

@ -0,0 +1,261 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="573278bf-2cdd-4afd-973a-1660f3364e9d" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/orchestra/api/actions.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/api/actions.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/api/serializers.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/api/serializers.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/accounts/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/accounts/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/accounts/serializers.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/accounts/serializers.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/admin.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/admin.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/settings.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/settings.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/templates/bills/microspective.css" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/templates/bills/microspective.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/bills/templates/bills/microspective.html" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/bills/templates/bills/microspective.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/databases/admin.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/databases/admin.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/databases/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/databases/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/domains/admin.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/domains/admin.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/domains/backends.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/domains/backends.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/domains/forms.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/domains/forms.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/domains/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/domains/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/domains/settings.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/domains/settings.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/domains/validators.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/domains/validators.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/lists/api.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/lists/api.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/lists/backends.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/lists/backends.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/lists/serializers.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/lists/serializers.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/mailboxes/backends.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/mailboxes/backends.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/orchestration/backends.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/orchestration/backends.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/payments/actions.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/payments/actions.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/payments/methods/sepadirectdebit.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/payments/methods/sepadirectdebit.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/saas/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/saas/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/saas/services/helpers.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/saas/services/helpers.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/systemusers/backends.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/systemusers/backends.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/systemusers/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/systemusers/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/webapps/backends/php.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/webapps/backends/php.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/webapps/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/webapps/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/webapps/options.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/webapps/options.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/webapps/serializers.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/webapps/serializers.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/webapps/signals.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/webapps/signals.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/websites/admin.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/websites/admin.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/websites/backends/apache.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/websites/backends/apache.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/orchestra/contrib/websites/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/orchestra/contrib/websites/models.py" afterDir="false" />
</list>
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileEditorManager">
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300">
<file pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/orchestra/contrib/lists/backends.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="589">
<caret line="124" column="4" lean-forward="true" selection-start-line="124" selection-start-column="4" selection-end-line="124" selection-end-column="4" />
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/orchestra/contrib/orchestration/backends.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="4256">
<caret line="224" column="8" selection-start-line="224" selection-start-column="8" selection-end-line="224" selection-end-column="8" />
</state>
</provider>
</entry>
</file>
</leaf>
</component>
<component name="FindInProjectRecents">
<findStrings>
<find>check_origin</find>
<find>updated_</find>
<find>UPDATED_CHECK_ORIGIN</find>
</findStrings>
<dirStrings>
<dir>$PROJECT_DIR$/orchestra/contrib/lists</dir>
</dirStrings>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="IdeDocumentHistory">
<option name="CHANGED_PATHS">
<list>
<option value="$PROJECT_DIR$/orchestra/contrib/lists/backends.py" />
</list>
</option>
</component>
<component name="ProjectConfigurationFiles">
<option name="files">
<list>
<option value="$PROJECT_DIR$/.idea/orchestra.iml" />
<option value="$PROJECT_DIR$/.idea/misc.xml" />
<option value="$PROJECT_DIR$/.idea/modules.xml" />
<option value="$PROJECT_DIR$/.idea/vcs.xml" />
<option value="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" />
</list>
</option>
</component>
<component name="ProjectFrameBounds" extendedState="6">
<option name="x" value="959" />
<option name="width" value="961" />
<option name="height" value="1056" />
</component>
<component name="ProjectView">
<navigator proportions="" version="1">
<foldersAlwaysOnTop value="true" />
</navigator>
<panes>
<pane id="ProjectPane">
<subPane>
<expand>
<path>
<item name="orchestra" type="b2602c69:ProjectViewProjectNode" />
<item name="orchestra" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="orchestra" type="b2602c69:ProjectViewProjectNode" />
<item name="orchestra" type="462c0819:PsiDirectoryNode" />
<item name="orchestra" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="orchestra" type="b2602c69:ProjectViewProjectNode" />
<item name="orchestra" type="462c0819:PsiDirectoryNode" />
<item name="orchestra" type="462c0819:PsiDirectoryNode" />
<item name="contrib" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="orchestra" type="b2602c69:ProjectViewProjectNode" />
<item name="orchestra" type="462c0819:PsiDirectoryNode" />
<item name="orchestra" type="462c0819:PsiDirectoryNode" />
<item name="contrib" type="462c0819:PsiDirectoryNode" />
<item name="lists" type="462c0819:PsiDirectoryNode" />
</path>
</expand>
<select />
</subPane>
</pane>
<pane id="Scope" />
</panes>
</component>
<component name="PropertiesComponent">
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.path.for.package.eslint" value="project" />
<property name="node.js.path.for.package.tslint" value="project" />
<property name="node.js.selected.package.eslint" value="(autodetect)" />
<property name="node.js.selected.package.tslint" value="(autodetect)" />
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
</component>
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
<component name="RunManager">
<configuration name="orchestra" type="Python.DjangoServer" factoryName="Django server">
<module name="orchestra" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="launchJavascriptDebuger" value="false" />
<option name="port" value="8000" />
<option name="host" value="" />
<option name="additionalOptions" value="" />
<option name="browserUrl" value="" />
<option name="runTestServer" value="false" />
<option name="runNoReload" value="false" />
<option name="useCustomRunCommand" value="false" />
<option name="customRunCommand" value="" />
<method v="2" />
</configuration>
</component>
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="573278bf-2cdd-4afd-973a-1660f3364e9d" name="Default Changelist" comment="" />
<created>1582799032840</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1582799032840</updated>
<workItem from="1582799053215" duration="2986000" />
<workItem from="1582882260836" duration="4157000" />
<workItem from="1582890671958" duration="815000" />
</task>
<servers />
</component>
<component name="TimeTrackingManager">
<option name="totallyTimeSpent" value="7958000" />
</component>
<component name="ToolWindowManager">
<frame x="-1" y="0" width="1922" height="1057" extended-state="6" />
<editor active="true" />
<layout>
<window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.124068156" />
<window_info id="Structure" order="1" side_tool="true" weight="0.25" />
<window_info id="Favorites" order="2" side_tool="true" />
<window_info anchor="bottom" id="Message" order="0" />
<window_info anchor="bottom" id="Find" order="1" />
<window_info anchor="bottom" id="Run" order="2" />
<window_info anchor="bottom" id="Debug" order="3" weight="0.4" />
<window_info anchor="bottom" id="Cvs" order="4" weight="0.25" />
<window_info anchor="bottom" id="Inspection" order="5" weight="0.4" />
<window_info anchor="bottom" id="TODO" order="6" />
<window_info anchor="bottom" id="Docker" order="7" show_stripe_button="false" />
<window_info anchor="bottom" id="Version Control" order="8" />
<window_info anchor="bottom" id="Database Changes" order="9" />
<window_info anchor="bottom" id="Event Log" order="10" side_tool="true" />
<window_info anchor="bottom" id="Terminal" order="11" />
<window_info anchor="bottom" id="Python Console" order="12" />
<window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" />
<window_info anchor="right" id="Ant Build" order="1" weight="0.25" />
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" />
<window_info anchor="right" id="SciView" order="3" />
<window_info anchor="right" id="Database" order="4" />
</layout>
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="1" />
</component>
<component name="editorHistoryManager">
<entry file="file://$PROJECT_DIR$/orchestra/contrib/lists/backends.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="589">
<caret line="124" column="4" lean-forward="true" selection-start-line="124" selection-start-column="4" selection-end-line="124" selection-end-column="4" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/orchestra/contrib/orchestration/backends.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="4256">
<caret line="224" column="8" selection-start-line="224" selection-start-column="8" selection-end-line="224" selection-end-column="8" />
</state>
</provider>
</entry>
</component>
</project>

View File

@ -1,25 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## master
## [0.2.0] 2022-03-11
- [added] Language selector.
- [added] Help link.
- [changed] Order bills by creation date DESC.
## [0.2.0-beta1] 2022-02-05
- [added] Write operations on mails section (addresses, mailboxes and forward).
- [changed] Include @pangea.org mail addresses (#4).
- [fixed] Error on login when user never has logged into the system (#6).
- [fixed] Dashboard layout issues on tablet and mobile.
## [0.1] - 2020-01-29
- Login & logout methods using backend as auth method
- Base template with services sidebar menu
- [added] Services views: mail, mailing list, SaaS, databases, domains and websites.
- [added] Profile and billing views.
- [added] Catalan and Spanish locales

View File

@ -41,7 +41,7 @@ This deployment is **not suitable for production** but more than enough for chec
```bash ```bash
# Create and activate a Python virtualenv # Create and activate a Python virtualenv
# Make sure python3.x-venv package is installed on your system # Make sure python3.x-venv package is installed on your system
python3 -m venv env-django-orchestra python3 -mvenv env-django-orchestra
source env-django-orchestra/bin/activate source env-django-orchestra/bin/activate
# Install Orchestra and its dependencies # Install Orchestra and its dependencies

View File

@ -55,7 +55,6 @@ INSTALLED_APPS = [
'orchestra.contrib.vps', 'orchestra.contrib.vps',
'orchestra.contrib.saas', 'orchestra.contrib.saas',
'orchestra.contrib.miscellaneous', 'orchestra.contrib.miscellaneous',
'orchestra.contrib.musician',
# Third-party apps # Third-party apps
'django_extensions', 'django_extensions',
@ -71,7 +70,6 @@ INSTALLED_APPS = [
'passlib.ext.django', 'passlib.ext.django',
'django_countries', 'django_countries',
# 'debug_toolbar', # 'debug_toolbar',
'bootstrap4',
# Django.contrib # Django.contrib
'django.contrib.auth', 'django.contrib.auth',
@ -221,24 +219,8 @@ EMAIL_BACKEND = 'orchestra.contrib.mailer.backends.EmailBackend'
DATA_UPLOAD_MAX_NUMBER_FIELDS = None DATA_UPLOAD_MAX_NUMBER_FIELDS = None
############################
## MUSICIAN CONFIGURATION ##
############################
# Direcction than go when you login
LOGIN_REDIRECT_URL = 'musician:dashboard'
# Where requests are redirected for login
LOGIN_URL = 'musician:login'
# The URL or named URL pattern where requests are redirected after logout
LOGOUT_REDIRECT_URL = 'musician:login'
USER_SUPPORT_EMAIL = "support@example.com"
################################# #################################
## 3RD PARTY APPS CONFIGURATION ## ## 3RD PARTY APPS CONIGURATION ##
################################# #################################
# Admin Tools # Admin Tools
@ -251,7 +233,6 @@ FLUENT_DASHBOARD_ICON_THEME = '../orchestra/icons'
# Django-celery # Django-celery
import djcelery import djcelery
djcelery.setup_loader() djcelery.setup_loader()
CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'

View File

@ -1,82 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:08
from __future__ import unicode_literals
import django.contrib.auth.models
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import orchestra.contrib.accounts.models
class Migration(migrations.Migration):
dependencies = [
('systemusers', '0001_initial'),
('auth', '0006_require_contenttypes_0002'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('username', models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username')),
('short_name', models.CharField(blank=True, max_length=64, verbose_name='short name')),
('full_name', models.CharField(max_length=256, verbose_name='full name')),
('email', models.EmailField(help_text='Used for password recovery', max_length=254, verbose_name='email address')),
('type', models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type')),
('language', models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language')),
('comments', models.TextField(blank=True, max_length=256, verbose_name='comments')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('main_systemuser', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts_main', to='systemusers.SystemUser')),
],
options={
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.AlterModelManagers(
name='account',
managers=[
('objects', orchestra.contrib.accounts.models.AccountManager()),
],
),
migrations.AlterField(
model_name='account',
name='language',
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
),
migrations.AlterField(
model_name='account',
name='type',
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='account',
name='username',
field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid username.', 'invalid')], verbose_name='username'),
),
migrations.AlterField(
model_name='account',
name='language',
field=models.CharField(choices=[('EN', 'English')], default='EN', max_length=2, verbose_name='language'),
),
migrations.AlterField(
model_name='account',
name='type',
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('COMPANY', 'Company'), ('PUBLICBODY', 'Public body'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='account',
name='main_systemuser',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts_main', to='systemusers.SystemUser'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 2.2.24 on 2024-07-11 12:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='account',
name='language',
field=models.CharField(choices=[('CA', 'Catalan'), ('ES', 'Spanish'), ('EN', 'English')], default='CA', max_length=2, verbose_name='language'),
),
migrations.AlterField(
model_name='account',
name='type',
field=models.CharField(choices=[('INDIVIDUAL', 'Individual'), ('ASSOCIATION', 'Association'), ('CUSTOMER', 'Customer'), ('STAFF', 'Staff'), ('FRIEND', 'Friend')], default='INDIVIDUAL', max_length=32, verbose_name='type'),
),
]

View File

@ -1,42 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBBBBBBBB BBBBBB BBBBBBBBBBBB
BBBBB BBBBBBBBBBB
XXXX XXXXXXXXXXXXXXXXXXXX
XX XXXXXXBBB BBBBBBBBBBBBBXX gettext(u'Home') XXXX
BB BBBBBBBBBBBB
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFFFFFFFFFFFXXXX
BBBB
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX BB BBBBBBBBBBBBBBBBBBBBBXX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFXXXXBBBBFFFFFFFFBBBBB
BBBBB
BB BBBBBBBBBBB
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXX gettext(u'Select %(name)s account') SSSSSS SSSSSSSS SSSSSSSXXXX
BBBBB
XXXXXXXX BB BBB gettext(u'Add') BBBBFFFFFFFFFFFFFFFFFFBBBBB
XXXXXX
BBBBBBBB
BBBBB BBBBBBBBBBBBBBBBBB
BB BBBBBBBB
XXXXXXXXXXX XXXXXXXXXXXX XXXXXXXXXXXXXXXXXX X XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX XXXX X X XXXX
XXXXXXX XXXXXXXX XXXXXXXXX gettext(u'Services') XXXXXXXXX
BBB BBBBBBB BB BBBBBBBB
XXXXXXX XXXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXXXXXXXFFFFFFFFXXXXXXXXX
BBBBBB
XXXXXXXXXXXXXX
BBBBB
BB BBBBBBBB
XXXXXXXXXXX XXXXXXXXXXXX XXXXXXXXXXXXXXXXXX X XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX XXXX XXX XXX XXXXXX
XXXXXXX XXXXXXXX XXXXXXXXX gettext(u'Accounts') XXXXXXXXX
BBB BBBBBBB BB BBBBBBBB
XXXXXXX XXXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXXXXXXXFFFFFFFFXXXXXXXXX
BBBBBB
XXXXXXXXXXXXXX
BBBBB
BBBBBBBB

View File

@ -1,49 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBBBBBBBB BBBBBBBBBB
BBBBB BBBBBBBBBBB
XXXX XXXXXXXXXXXXXXXXXXXX
XX XXXXXXBBB BBBBBBBBBBBBBXX gettext(u'Home') XXXX
BB BBBBBBB
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFFFFFFFFFFFXXXX
BBBB
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
BBBBB
XXXXXXXX FFFFFFFF
XXXXXX
BBBBBBBB
BBBBB BBBBBBBBBBBBBBBBBB
XXXX
BBB BBBBBBBBBBBBBBBBBBBBBBBBBBB BB BBBBBBB
XX XXXXXXBBBBBBBBBBBBBBBBBBBBB BBBBBBB BBBBBBBB BBBBBBBBX XXXXXXXXXXXXXXXX
BB BBBBBBBBBBBB
gettext(u'Add %(name)s') SSS SSSSSSSS
BBBB
gettext(u'Add %(account)s %(name)s') SSS SSSSSSSSSSS SSSSSSSS
BBBBB
XXXX
XXXXX
BBBBBBBB
BBBBB BBBBBBB
BB BBBBBBBBBBBBBB
XXXX XXXXXXXXXXXXXXXXXXXXXXX
XXXX gettext(u'Filter') XXXXX
BB BBBBBBB
XXXX gettext(u'By account') XXXXX
XXXX
XXX BB BBB BBBBBBBBBBBBXXXXXXXXXXXXXXXXBBBBBXXX XXXXXXXXXXXXXXXXXFFFFFFFFFFFFFFFFFFXXXXXXXXX
XXX BB BBBBBBBBBBBBXXXXXXXXXXXXXXXXBBBBBXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXX
BBBBB
BBB BBBB BB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BB BBBBBBBBBB
XXXXXX
BBBBB
BBBBBBBB

View File

@ -1,39 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBB BBBBBBBBBB
BBBBB BBBBBBB
BB BBBBBBBBBBBBB
XXX gettext(u"Deleting the selected %(objects_name)s would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:") SSSSSSSS SSS SSSSSSSS SSSSSSSSSSSSSSSS SSSSS SSSSSS SS SSSSSSSS SSSSSSS SSSSSSSS SSS SSSS SSSSSSS SSSSSSS SSSS SSSSSSSSSS SS SSSSSS SSS SSSSSSSSS SSSSS SS SSSSSSSSXXXX
XXXX
BBB BBB BB BBBBBBBBBBBBB
XXXXXXXXX
BBBBBB
XXXXX
BBBB BBBBBBBBB
XXX gettext(u'Deleting the selected %(objects_name)s would require deleting the following protected related objects:') SSSSSSSS SSS SSSSSSSS SSSSSSSSSSSSSSSS SSSSS SSSSSSS SSSSSSSS SSS SSSSSSSSS SSSSSSSSS SSSSSSS SSSSSSSSXXXX
XXXX
BBB BBB BB BBBBBBBBB
XXXXXXXXX
BBBBBB
XXXXX
BBBB
XXX gettext(u'Are you sure you want to delete the selected %(objects_name)s? All of the following objects and their related items will be deleted:') SSS SSS SSSS SSS SSSS SS SSSSSS SSS SSSSSSSS SSSSSSSSSSSSSSSSS SSS SS SSS SSSSSSSSS SSSSSSS SSS SSSSS SSSSSSS SSSSS SSSS SS SSSSSSSSXXXX
BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
XXXX gettext(u'Objects') XXXXX
BBB BBBBBBBBBBBBBBBB BB BBBBBBBBBBBBBBBBB
XXXXFFFFFFFFFFFFFFXXXXX
BBBBBB
XXXXX XXXXXXXXX XXXXXXXXXXXXXXBBBBBBBBBB
XXXXX
BBB BBB BB BBBBBBBB
XXXXXX XXXXXXXXXXXXX XXXXXXX XXXXXXXFFFFFFFFFFX XX
BBBBBB
XXXXXX XXXXXXXXXXXXX XXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XX
XXXXXX XXXXXXXXXXXXX XXXXXXXXXXX XXXXXXXXXXX XX
XXXXXX XXXXXXXXXXXXX XXXXXXX gettext(u"Yes, I'm sure") X XX
XX XXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXX XXXXXXXXXXXXX gettext(u'No, take me back') XXXX
XXXXXX
XXXXXXX
BBBBB
BBBBBBBB

View File

@ -1,35 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBB BBBBBBBBBB
BBBBB BBBBBBBBB XXXX XXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXBBBBBBBB
BBBBB BBBBBBBBBBB
XXXX XXXXXXXXXXXXXXXXXXXX
XX XXXXXXBBB BBBBBBBBBBBBBXX gettext(u'Home') XXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFXXXX
XXXXXXXX BB BBBBBBB gettext(u'Disable %(objects_name)s') SSSSSSS SSSSSSSSSSSSSSSSBBBB gettext(u'Enable %(objects_name)s') SSSSSS SSSSSSSSSSSSSSSSBBBBB
XXXXXX
BBBBBBBB
BBBBB BBBBBBB
BB BBBBBBBXXX gettext(u'Are you sure you want to disable selected %(objects_name)s?') SSS SSS SSSS SSS SSSS SS SSSSSSS SSSSSSSS SSSSSSSSSSSSSSSSSXXXX
BBBBXXX gettext(u'Are you sure you want to enable selected %(objects_name)s?') SSS SSS SSSS SSS SSSS SS SSSSSS SSSSSSSS SSSSSSSSSSSSSSSSSXXXX
BBBBB
XXXX gettext(u'Objects') XXXXX
BBB BBBBBBBBBBBBBBBB BB BBBBBBBBBBBBBBBBB
XXXXFFFFFFFFFFFFFFXXXXX
BBBBBB
XXXXX XXXXXXXXX XXXXXXXXXXXXXXBBBBBBBBBB
XXXXX
BBB BBB BB BBBBBBBB
XXXXXX XXXXXXXXXXXXX XXXXXXX XXXXXXXFFFFFFFFFFX XX
BBBBBB
XXXXXX XXXXXXXXXXXXX XXXXXXXXXXXXX XXXXXXXX XX
XXXXXX XXXXXXXXXXXXX XXXXXXXXXXX XXXXXXXXXXX XX
XXXXXX XXXXXXXXXXXXX XXXXXXX gettext(u"Yes, I'm sure") X XX
XX XXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXX XXXXXXXXXXXXX gettext(u'No, take me back') XXXX
XXXXXX
XXXXXXX
BBBBBBBB

View File

@ -1,13 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBBBBBBBB
BBBBB BBBBBBBBBBB
XXXX XXXXXXXXXXXXXXXXXXXX
XX XXXXXXBBB BBBBBBBBBBBBBXX gettext(u'Home') XXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFXXXX
XXXXXXXX gettext(u'Select %(name)s account') SSSSSS SSSSSSSS SSSSSSS
XXXXXX
BBBBBBBB

View File

@ -1,84 +0,0 @@
BBBB BBBB BBBBBBBBBB BBBBB
XXXXXX
XXXXXX
XXXXXXXBBBBB BBBBBXXXXXXX XXXXXXX XXXXXXBBBBBBBBXXXXXXXX
XXXXX XXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX
BBBBB BBBBBBBBBBBB
XXXXXX XXXXXXXXXXXXXXXX
XXXX X
XXXXXXXXXX XXXXXX
XXXXXXX XX XXXX XXXXXXXXXXX
XXXXXX XXXX XXXXXXXXXXX
XXXXXXXXXXXX XXXXXX XXXXXXXXXXX XXXXXX XXXXXXX XXXXXX XXXXXXXXXXX
XXXXXXXXXX XXXXX
XXXXXX XXXXX
X
XXXXX X
XXXXXX XXXXXX
XXXXXX XXXXXXXX XXXX XXXXX
X
XXXXXXXXXXXXXXXX X
XXXXXXX XXX XXX XXXX XXXXX
X
XXXXXXXXXXX X
XXXXXXXXXXXXXXXX XXXXX
XXXXXXXXXXXX XXXXX
XXXXXX XXXXX
X
XXXXXXXXX X
XXXXXXXX XXXX
XXXXXXX XXX XXX XXXX XXXXX
X
XXXXXXXX X
XXXXXXXXXXX XXXXX
X
XX X
XXXXXXXXXXX XXXXX
X
X X
XXXXXXXXXXXXXXXX XXXXX
XXXXXX XXXXXXX XXXX XXXXX
X
XXXXXXXX
XXXXXXX
XXXXXX
XXXX XXXXXXXXXX gettext(u'Service report generated on') FFFFXXXXXX
BBB BBBBBBBB BBBBB BB BBBBBBBB
XXXX X XX XXXXXXFFFFFFFFFXXXXXXXXXXX
XXXX
XXXX XXXXXXXXXXXXXXXXXXXXXXXX
gettext(u'account registered on') FFFFXXXX
XXX XXXXXXXXXXXXXXXXX
XXX XXXXXXXXXXXXXXXXXXX gettext(u'Resources') XXXXX
BB BBBBBBBBBBBBBBBBB
XXXX
BBB BBBBBBBB BB BBBBBBBBBBBBBBBBB
XXXXXX XXXXXXFFFFFFFFFXX BB BBBBBBBBBBBBB BB BBBBXXXXX XXXXXXX gettext(u'Used') XXXXXXXXXBBBBBBB BBBBBBBBBBBBBBBBBB BB BBBBBB BBBBBBBBBBBBB BB BBBB X BBBBBXXXXX XXXXXXX gettext(u'Allocated') XXXXXXXXXBBBBBXXXX XXXXX
BBBBBB
XXXXX
BBBBB
BBB BBBBB BBBBBBB BB BBBBB
XXX XXXXXXXXXXXXXXXXXXXXX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXXXXXXXXXXFFFFFFFFXXXXXXXXX
XXXX
BBB BBB BB BBBBBBB
XXX XXXXXXXXXXXXXXXXXX XXXXXXFFFFFFFFFXXXXXX
BB BBB BBBBBBBBBBBB X gettext(u'disabled') XBBBBB
FFFFFFFF
BB BBBBBBBBBBBBB
XXXX
BBB BBBBBBBB BB BBBBBBBBBBBBB
XXXXXX XXXXXXFFFFFFFFFXX BB BBBBBBBBBBBBB BB BBBBXXXXX XXXXXXX gettext(u'Used') XXXXXXXXXBBBBBBB BBBBBBBBBBBBBBBBBB BB BBBBBB BBBBBBBBBBBBB BB BBBB X BBBBBXXXXX XXXXXXX gettext(u'Allocated') XXXXXXXXXBBBBBXXXX XXXXX
BBBBBB
XXXXX
BBBBB
XXXXX
BBBBBB
XXXXX
BBBBBB
XXXXX
XXXXXX
BBBBBB
XXXXXXX
XXXXXXX

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,9 +29,6 @@ class MySQLController(ServiceController):
# Create database and re-set permissions # Create database and re-set permissions
mysql -e 'CREATE DATABASE `%(database)s`;' || true mysql -e 'CREATE DATABASE `%(database)s`;' || true
mysql mysql -e 'DELETE FROM db WHERE db = "%(database)s";'\ mysql mysql -e 'DELETE FROM db WHERE db = "%(database)s";'\
# wait to create user
sleep 1.5
""") % context """) % context
) )
for user in database.users.all(): for user in database.users.all():
@ -181,7 +178,7 @@ class MysqlDisk(ServiceMonitor):
def get_context(self, db): def get_context(self, db):
context = { context = {
'db_name': db.name, 'db_name': db.name,
'db_dirname': db.name.replace('-', '@002d').replace('.', '@002e'), 'db_dirname': db.name.replace('-', '@002d'),
'db_id': db.pk, 'db_id': db.pk,
'db_type': db.type, 'db_type': db.type,
} }

View File

@ -8,7 +8,6 @@ from django.utils.translation import gettext_lazy as _
from orchestra.core import validators from orchestra.core import validators
from .models import DatabaseUser, Database from .models import DatabaseUser, Database
from .settings import DATABASES_SERVERS
class DatabaseUserCreationForm(forms.ModelForm): class DatabaseUserCreationForm(forms.ModelForm):
@ -23,11 +22,6 @@ class DatabaseUserCreationForm(forms.ModelForm):
model = DatabaseUser model = DatabaseUser
fields = ('username', 'account', 'type') fields = ('username', 'account', 'type')
def __init__(self, *args, **kwargs):
super(DatabaseUserCreationForm, self).__init__(*args, **kwargs)
qsServer = self.fields['target_server'].queryset.filter(name__in=DATABASES_SERVERS)
self.fields['target_server'].queryset = qsServer
def clean_password2(self): def clean_password2(self):
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
@ -80,10 +74,6 @@ class DatabaseCreationForm(DatabaseUserCreationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseCreationForm, self).__init__(*args, **kwargs) super(DatabaseCreationForm, self).__init__(*args, **kwargs)
account_id = self.initial.get('account', self.initial_account) account_id = self.initial.get('account', self.initial_account)
qsServer = self.fields['target_server'].queryset.filter(name__in=DATABASES_SERVERS)
self.fields['target_server'].queryset = qsServer
if account_id: if account_id:
qs = self.fields['user'].queryset.filter(account=account_id).order_by('username') qs = self.fields['user'].queryset.filter(account=account_id).order_by('username')
choices = [ (u.pk, "%s (%s) (%s)" % (u, u.get_type_display(), str(u.target_server.name) )) for u in qs ] choices = [ (u.pk, "%s (%s) (%s)" % (u, u.get_type_display(), str(u.target_server.name) )) for u in qs ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Generated by Django 2.2.28 on 2023-06-28 17:06
# Generated by Django 1.10.5 on 2021-04-22 11:25
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -10,69 +8,39 @@ import orchestra.core.validators
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel(
name='Database',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
],
),
migrations.CreateModel( migrations.CreateModel(
name='DatabaseUser', name='DatabaseUser',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=16, validators=[orchestra.core.validators.validate_name], verbose_name='username')), ('username', models.CharField(max_length=16, validators=[orchestra.core.validators.validate_name], verbose_name='username')),
('password', models.CharField(max_length=256, verbose_name='password')), ('password', models.CharField(max_length=256, verbose_name='password')),
('type', models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type')), ('type', models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')), ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databaseusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
], ],
options={ options={
'verbose_name_plural': 'DB users', 'verbose_name_plural': 'DB users',
'unique_together': {('username', 'type')},
}, },
), ),
migrations.AddField( migrations.CreateModel(
model_name='database', name='Database',
name='users', fields=[
field=models.ManyToManyField(blank=True, related_name='databases', to='databases.DatabaseUser', verbose_name='users'), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
), ('name', models.CharField(max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
migrations.AlterUniqueTogether( ('type', models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type')),
name='databaseuser', ('comments', models.TextField(blank=True, default='')),
unique_together=set([('username', 'type')]), ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='databases', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
), ('users', models.ManyToManyField(blank=True, related_name='databases', to='databases.DatabaseUser', verbose_name='users')),
migrations.AlterUniqueTogether( ],
name='database', options={
unique_together=set([('name', 'type')]), 'unique_together': {('name', 'type')},
), },
migrations.AlterField(
model_name='database',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='databaseuser',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AddField(
model_name='database',
name='comments',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='database',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='databaseuser',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('postgres', 'PostgreSQL')], default='mysql', max_length=32, verbose_name='type'),
), ),
] ]

View File

@ -1,23 +0,0 @@
# Generated by Django 2.2.24 on 2024-07-11 12:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('databases', '0006_auto_20230705_1237'),
]
operations = [
migrations.AlterField(
model_name='database',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
migrations.AlterField(
model_name='databaseuser',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=32, verbose_name='type'),
),
]

View File

@ -27,12 +27,3 @@ DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST',
DATABASES_MYSQL_DB_DIR = Setting('DATABASES_MYSQL_DB_DIR', DATABASES_MYSQL_DB_DIR = Setting('DATABASES_MYSQL_DB_DIR',
'/var/lib/mysql', '/var/lib/mysql',
) )
DATABASES_SERVERS = Setting('DATABASES_SERVERS', (
'wpmu',
'mysql.pangea.lan',
'web-11.pangea.lan',
'web-12.pangea.lan',
)
)

View File

@ -1,462 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-06 07:58+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:25
#, python-format
msgid "%s zone content"
msgstr "%s contingut zona"
#: actions.py:29 templates/admin/domains/domain/change_form.html:7
#: templates/admin/domains/domain/view_zone.html:12
msgid "View zone"
msgstr "Veure zona"
#: actions.py:53
msgid ""
"This subdomain was not explicitly selected but has been automatically added "
"to this list."
msgstr ""
"Aquest subdomini no va ser seleccionat explícitament, però s'ha afegit automàticament"
"A aquesta llista."
#: actions.py:88
msgid "Records for one selected domain have been updated."
msgstr "S'han actualitzat els registres d'un domini seleccionat."
#: actions.py:89
#, python-format
msgid "Records for %i selected domains have been updated."
msgstr "S'han actualitzat els registres dels dominis seleccionars %i "
#: actions.py:96 actions.py:97
msgid "Edit records"
msgstr "Editar registres"
#: actions.py:112
msgid "Set SOA on subdomains is not possible."
msgstr "Configurar SOA en subdominis no és possible."
#: actions.py:120
#, python-format
msgid "SOA set %s"
msgstr ""
#: actions.py:131
msgid "SOA record for one domain has been updated."
msgstr "El registre SOA per a un domini s'ha actualitzat."
#: actions.py:132
#, python-format
msgid "SOA record for %s domains has been updated."
msgstr "S'ha actualitzat el rècord SOA per als dominis de %s"
#: actions.py:139 actions.py:152
msgid "Set SOA for selected domains"
msgstr "Configurar SOA pels dominis seleccionats"
#: actions.py:141
msgid "Set SOA"
msgstr "Afegir SOA"
#: admin.py:28
msgid "Extra records"
msgstr "Registres Extra"
#: admin.py:36
msgid "Subdomains"
msgstr "Subdominis"
#: admin.py:39
msgid "Name"
msgstr "Nom"
#: admin.py:44
msgid "Declared records"
msgstr "Registres declarats"
#: admin.py:78 models.py:27
msgid "name"
msgstr "nom"
#: admin.py:83
msgid "Is top"
msgstr ""
#: admin.py:96
msgid "Edit website"
msgstr "Editar website"
#: admin.py:105
msgid "Add website"
msgstr "Crear website"
#: admin.py:107
#, python-format
msgid "No website %s"
msgstr ""
#: admin.py:110
msgid "Websites"
msgstr ""
#: admin.py:119
msgid "Add address"
msgstr ""
#: admin.py:127
#, python-format
msgid "No address %s"
msgstr ""
#: admin.py:129
msgid "Addresses"
msgstr ""
#: admin.py:151 admin.py:158
msgid "Implicit records"
msgstr "Registres implicits"
#: admin.py:165
msgid "SOA"
msgstr ""
#: admin.py:168
msgid ""
"SOA (Start of Authority) records are used to determine how the zone "
"propagates to the secondary nameservers."
msgstr ""
"Els registres SOA (Start of Authority) s'utilitzen per determinar com la zona"
"Es propaga als servidors de noms secundaris."
#: backends.py:22
msgid "Bind9 master domain"
msgstr ""
#: backends.py:184
msgid "Bind9 slave domain"
msgstr ""
#: filters.py:7
msgid "top domains"
msgstr ""
#: filters.py:12
msgid "Top domains"
msgstr ""
#: filters.py:22
msgid "has websites"
msgstr ""
#: filters.py:27
msgid "True"
msgstr ""
#: filters.py:28
msgid "False"
msgstr ""
#: filters.py:41
msgid "has addresses"
msgstr ""
#: forms.py:14
msgid "Names"
msgstr ""
#: forms.py:15
msgid ""
"Fully qualified domain name per line. All domains will have the provided "
"account and records."
msgstr ""
"Nom de domini completament qualificat per línia. Tots els dominis tindran el subministrament"
"Compte i registres."
#: forms.py:29
#, python-format
msgid "%s domain name provided multiple times."
msgstr "El nom de domini %s es va proporcionar diverses vegades."
#: forms.py:65
msgid "An account should be provided for top domain names."
msgstr ""
#: forms.py:72
msgid "Provided domain names belong to different accounts."
msgstr "Els noms de domini pertanyen a diferents comptes"
#: forms.py:118
#, python-format
msgid ""
"%s: Hosts can not have underscore character '_', consider providing a SRV, "
"CNAME or TXT record."
msgstr ""
"%s: Els Hosts no poden tenir un caràcter '_', considereu proporcionar un SRV"
"Registre CNAME o TXT."
#: forms.py:142
msgid "Clear refresh"
msgstr ""
#: forms.py:143
msgid "Remove custom refresh value for all selected domains."
msgstr ""
#: forms.py:145
msgid "Clear retry"
msgstr ""
#: forms.py:146
msgid "Remove custom retry value for all selected domains."
msgstr ""
#: forms.py:148
msgid "Clear expire"
msgstr ""
#: forms.py:149
msgid "Remove custom expire value for all selected domains."
msgstr ""
#: forms.py:151
msgid "Clear min TTL"
msgstr ""
#: forms.py:152
msgid "Remove custom min TTL value for all selected domains."
msgstr ""
#: models.py:28
msgid "Domain or subdomain name."
msgstr "nom de Domini o Subdomini"
#: models.py:33
msgid "Account"
msgstr "Compte"
#: models.py:34
msgid "Automatically selected for subdomains."
msgstr "Seleccionat automàticament per a subdominis."
#: models.py:36
msgid "top domain"
msgstr ""
#: models.py:37
msgid "serial"
msgstr ""
#: models.py:38
msgid "A revision number that changes whenever this domain is updated."
msgstr "Un número de revisió que canvia sempre que aquest domini s'actualitzi."
#: models.py:39
msgid "refresh"
msgstr ""
#: models.py:41
#, python-format
msgid ""
"The time a secondary DNS server waits before querying the primary DNS "
"server's SOA record to check for changes. When the refresh time expires, the "
"secondary DNS server requests a copy of the current SOA record from the "
"primary. The primary DNS server complies with this request. The secondary "
"DNS server compares the serial number of the primary DNS server's current "
"SOA record and the serial number in it's own SOA record. If they are "
"different, the secondary DNS server will request a zone transfer from the "
"primary DNS server. The default value is <tt>%s</tt>."
msgstr ""
"El temps que un servidor DNS secundari espera abans de consultar el DNS primari"
"Registre SOA del servidor per comprovar si hi ha canvis. Quan el temps d'actualització caduca,"
"El servidor DNS secundari sol·licita una còpia del registre SOA actual del"
"Primària. El servidor DNS primari compleix aquesta sol·licitud. La secundària"
"El servidor DNS compara el número de sèrie del corrent principal del servidor DNS"
"Registre SOA i el número de sèrie del seu propi registre SOA. Si ho són"
"Diferent, el servidor DNS secundari sol·licitarà una transferència de zona des del"
"Servidor DNS primari. El valor per defecte és <tt>%s</tt>."
#: models.py:50
msgid "retry"
msgstr ""
#: models.py:52
#, python-format
msgid ""
"The time a secondary server waits before retrying a failed zone transfer. "
"Normally, the retry time is less than the refresh time. The default value is "
"<tt>%s</tt>."
msgstr ""
"El temps que un servidor secundari espera abans de tornar a tornar a transferir una zona fallida."
"Normalment, el temps de tornada és inferior al temps d'actualització. El valor predeterminat és"
"<tt>%s</tt>."
#: models.py:55
msgid "expire"
msgstr ""
#: models.py:57
#, python-format
msgid ""
"The time that a secondary server will keep trying to complete a zone "
"transfer. If this time expires prior to a successful zone transfer, the "
"secondary server will expire its zone file. This means the secondary will "
"stop answering queries. The default value is <tt>%s</tt>."
msgstr ""
"El temps que un servidor secundari continuarà intentant completar una zona"
"Transferència. Si aquesta vegada caduca abans d'una transferència de zona amb èxit,"
"El servidor secundari caducarà el seu fitxer de zona. Això significa que la voluntat secundària"
"Deixa de respondre a les consultes. El valor predeterminat és <tt>%s</tt>."
#: models.py:62
msgid "min TTL"
msgstr ""
#: models.py:64
#, python-format
msgid ""
"The minimum time-to-live value applies to all resource records in the zone "
"file. This value is supplied in query responses to inform other servers how "
"long they should keep the data in cache. The default value is <tt>%s</tt>."
msgstr ""
"El valor mínim de temps en viu s'aplica a tots els registres de recursos de la zona"
"Fitxer. Aquest valor es subministra a les respostes de consulta per informar a altres servidors com"
"Long han de mantenir les dades en memòria cau. El valor predeterminat és <tt>%s</tt>"
#: models.py:118
msgid "top domain with one subdomain"
msgstr ""
#: models.py:119
#, python-format
msgid "top domain with %d subdomains"
msgstr ""
#: models.py:121
msgid "subdomain"
msgstr "subdomini"
#: models.py:308
msgid "A (IPv4 address)"
msgstr ""
#: models.py:309
msgid "AAAA (IPv6 address)"
msgstr ""
#: models.py:329
msgid "domain"
msgstr "domini"
#: models.py:330
msgid "TTL"
msgstr ""
#: models.py:331
#, python-format
msgid "Record TTL, defaults to %s"
msgstr "Registre TTL, per defecte a %s"
#: models.py:333
msgid "type"
msgstr "tipus"
#: models.py:335
msgid "value"
msgstr "valor"
#: models.py:336
msgid "MX, NS and CNAME records sould end with a dot."
msgstr "Els registres de MX, NS i CNAME s'acaben amb un punt."
#: serializers.py:36
msgid "Can not create subdomains of other users domains"
msgstr "No es pot crear subdominis d'altres dominis d'usuaris"
#: templates/admin/domains/domain/change_form.html:11
msgid "History"
msgstr "Historial"
#: templates/admin/domains/domain/change_form.html:13
msgid "View on site"
msgstr "Veure al lloc"
#: templates/admin/domains/domain/view_zone.html:8
msgid "Home"
msgstr ""
#: validators.py:28
msgid "This domain name is not allowed"
msgstr "Aquest nom de domini no està permès"
#: validators.py:37
msgid "Not a valid domain name."
msgstr "No és un nom de domini vàlid."
#: validators.py:46
#, python-format
msgid "%s is not an appropiate zone interval value"
msgstr "%s no és un valor d'interval de zona adequat"
#: validators.py:57
msgid ""
"Labels must start and end with a letter or digit, and have as interior "
"characters only letters, digits, and hyphen."
msgstr ""
"Les etiquetes han de començar i acabar amb una lletra o dígit i tenir en l'interior"
"Només caràcters lletres, dígits i guionet."
#: validators.py:61
msgid "Use a fully expanded domain name ending with a dot."
msgstr "Utilitzeu un nom de domini complet amb un punt final."
#: validators.py:64
msgid "Labels must be 63 characters or less."
msgstr "Les etiquetes han de tenir 63 caràcters o menys."
#: validators.py:68
msgid ""
"MX record format is 'priority domain.' tuple, with priority being a number."
msgstr ""
"El format de registre MX és 'domini prioritari'. Tuple, amb la prioritat que és un número "
#: validators.py:83 validators.py:95
#, python-format
msgid "%s is not an appropiate SRV record value"
msgstr "%s no és un valor de registre SRV Apropiat"
#: validators.py:111
#, python-format
msgid ""
"%s is not an appropiate CAA record value, sintax: 0-255 issue|issuewild|"
"iodef \"domain|mailto:email\""
msgstr ""
"%s no és un valor de registre CAA adequat, sintaxi: 0-255 issue|issuewild|"
"iodef \"domain|mailto:email\""
#: validators.py:134
msgid ""
"This record value contains spaces, you must enclose the string in double "
"quotes; otherwise, individual words will be separately quoted and break up "
"the record into multiple parts."
msgstr ""
"Aquest valor de registre conté espais, heu d'incloure la cadena ab cometes dobles"
"en cas contrari, les paraules individuals es citaran per separat i es trencaran"
"El registre en diverses parts"

View File

@ -1,461 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-05 10:11+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:25
#, python-format
msgid "%s zone content"
msgstr "%s contenido de zona"
#: actions.py:29 templates/admin/domains/domain/change_form.html:7
#: templates/admin/domains/domain/view_zone.html:12
msgid "View zone"
msgstr "Ver zona"
#: actions.py:53
msgid ""
"This subdomain was not explicitly selected but has been automatically added "
"to this list."
msgstr ""
"Este subdominio no fue seleccionado explícitamente, pero se ha agregado automáticamente"
"A esta lista"
#: actions.py:88
msgid "Records for one selected domain have been updated."
msgstr "Se han actualizado los registros del dominio seleccionado"
#: actions.py:89
#, python-format
msgid "Records for %i selected domains have been updated."
msgstr "Se han actualizado los registros para los %i dominios seleccionados"
#: actions.py:96 actions.py:97
msgid "Edit records"
msgstr "Editar Registros"
#: actions.py:112
msgid "Set SOA on subdomains is not possible."
msgstr "No es posible añadir el registro SOA en los subdominios"
#: actions.py:120
#, python-format
msgid "SOA set %s"
msgstr "Añadir SOA %s"
#: actions.py:131
msgid "SOA record for one domain has been updated."
msgstr "Se ha actualizado el registro SOA del dominio"
#: actions.py:132
#, python-format
msgid "SOA record for %s domains has been updated."
msgstr "Se actualizo el registro SOA para los dominios %s"
#: actions.py:139 actions.py:152
msgid "Set SOA for selected domains"
msgstr "Añadir SOA en los dominios seleccionados"
#: actions.py:141
msgid "Set SOA"
msgstr "Añadir SOA"
#: admin.py:28
msgid "Extra records"
msgstr "Registros extra"
#: admin.py:36
msgid "Subdomains"
msgstr "Subdominios"
#: admin.py:39
msgid "Name"
msgstr "Nombre"
#: admin.py:44
msgid "Declared records"
msgstr "Registros declarados"
#: admin.py:78 models.py:27
msgid "name"
msgstr "Nombre"
#: admin.py:83
msgid "Is top"
msgstr "Es top level"
#: admin.py:96
msgid "Edit website"
msgstr "Editar WebSite"
#: admin.py:105
msgid "Add website"
msgstr "Añadir WebSite"
#: admin.py:107
#, python-format
msgid "No website %s"
msgstr ""
#: admin.py:110
msgid "Websites"
msgstr ""
#: admin.py:119
msgid "Add address"
msgstr "Añadir dirección"
#: admin.py:127
#, python-format
msgid "No address %s"
msgstr ""
#: admin.py:129
msgid "Addresses"
msgstr "Direcciones"
#: admin.py:151 admin.py:158
msgid "Implicit records"
msgstr "Registros implicitos"
#: admin.py:165
msgid "SOA"
msgstr ""
#: admin.py:168
msgid ""
"SOA (Start of Authority) records are used to determine how the zone "
"propagates to the secondary nameservers."
msgstr ""
"Los registros SOA (inicio de la autoridad) se utilizan para determinar cómo la zona"
"Se propaga a los servidores de nombres secundarios"
#: backends.py:22
msgid "Bind9 master domain"
msgstr ""
#: backends.py:184
msgid "Bind9 slave domain"
msgstr ""
#: filters.py:7
msgid "top domains"
msgstr ""
#: filters.py:12
msgid "Top domains"
msgstr ""
#: filters.py:22
msgid "has websites"
msgstr ""
#: filters.py:27
msgid "True"
msgstr ""
#: filters.py:28
msgid "False"
msgstr ""
#: filters.py:41
msgid "has addresses"
msgstr ""
#: forms.py:14
msgid "Names"
msgstr "Nombres"
#: forms.py:15
msgid ""
"Fully qualified domain name per line. All domains will have the provided "
"account and records."
msgstr ""
"Nombre de dominio totalmente calificado por línea. Todos los dominios tendrán los proporcionados"
"Cuenta y registros"
#: forms.py:29
#, python-format
msgid "%s domain name provided multiple times."
msgstr "%s Nombre de dominio proporcionado varias veces."
#: forms.py:65
msgid "An account should be provided for top domain names."
msgstr ""
#: forms.py:72
msgid "Provided domain names belong to different accounts."
msgstr "Los nombres de dominio proporcionados pertenecen a diferentes cuentas"
#: forms.py:118
#, python-format
msgid ""
"%s: Hosts can not have underscore character '_', consider providing a SRV, "
"CNAME or TXT record."
msgstr ""
"%s: los hosts no pueden tener un carácter subrayado '_', considere proporcionar un SRV, "
"Registro CNAME o TXT."
#: forms.py:142
msgid "Clear refresh"
msgstr ""
#: forms.py:143
msgid "Remove custom refresh value for all selected domains."
msgstr ""
#: forms.py:145
msgid "Clear retry"
msgstr ""
#: forms.py:146
msgid "Remove custom retry value for all selected domains."
msgstr ""
#: forms.py:148
msgid "Clear expire"
msgstr ""
#: forms.py:149
msgid "Remove custom expire value for all selected domains."
msgstr ""
#: forms.py:151
msgid "Clear min TTL"
msgstr ""
#: forms.py:152
msgid "Remove custom min TTL value for all selected domains."
msgstr ""
#: models.py:28
msgid "Domain or subdomain name."
msgstr "Dominio o Subdominio"
#: models.py:33
msgid "Account"
msgstr "Cuenta"
#: models.py:34
msgid "Automatically selected for subdomains."
msgstr "Seleccionado automáticamente para subdominios"
#: models.py:36
msgid "top domain"
msgstr ""
#: models.py:37
msgid "serial"
msgstr ""
#: models.py:38
msgid "A revision number that changes whenever this domain is updated."
msgstr "Un número de revisión que cambia cada vez que se actualiza este dominio"
#: models.py:39
msgid "refresh"
msgstr "actualizar"
#: models.py:41
#, python-format
msgid ""
"The time a secondary DNS server waits before querying the primary DNS "
"server's SOA record to check for changes. When the refresh time expires, the "
"secondary DNS server requests a copy of the current SOA record from the "
"primary. The primary DNS server complies with this request. The secondary "
"DNS server compares the serial number of the primary DNS server's current "
"SOA record and the serial number in it's own SOA record. If they are "
"different, the secondary DNS server will request a zone transfer from the "
"primary DNS server. The default value is <tt>%s</tt>."
msgstr ""
"La hora en que un servidor DNS secundario espera antes de consultar el DNS primario"
"El registro SOA del servidor para verificar los cambios. Cuando la tiempo de actualización expira,"
"El servidor DNS secundario solicita una copia del registro SOA actual del"
"Primario. El servidor DNS principal cumple con esta solicitud. El secundario"
"El servidor DNS compara el número de serie del servidor DNS principal actual"
"SOA Record y el número de serie en su propio registro SOA. Si lo son"
"Diferente, el servidor DNS secundario solicitará una transferencia de zona del"
"Servidor DNS primario. El valor predeterminado es <tt>%s</tt>"
#: models.py:50
msgid "retry"
msgstr ""
#: models.py:52
#, python-format
msgid ""
"The time a secondary server waits before retrying a failed zone transfer. "
"Normally, the retry time is less than the refresh time. The default value is "
"<tt>%s</tt>."
msgstr ""
"El tiempo en que un servidor secundario espera antes de volver a intentar una transferencia de zona fallida"
"Normalmente, el tiempo de reintento es menor que el tiempo de actualización. El valor predeterminado es"
"<Tt>%s</tt>"
#: models.py:55
msgid "expire"
msgstr ""
#: models.py:57
#, python-format
msgid ""
"The time that a secondary server will keep trying to complete a zone "
"transfer. If this time expires prior to a successful zone transfer, the "
"secondary server will expire its zone file. This means the secondary will "
"stop answering queries. The default value is <tt>%s</tt>."
msgstr ""
"El momento en que un servidor secundario seguirá intentando completar una zona"
"Transferencia. Si este tiempo expira antes de una transferencia de zona exitosa,"
"El servidor secundario caducará su archivo de zona. Esto significa que el secundario lo hará"
"Deje de responder consultas. El valor predeterminado es <tt>%s</tt>"
#: models.py:62
msgid "min TTL"
msgstr ""
#: models.py:64
#, python-format
msgid ""
"The minimum time-to-live value applies to all resource records in the zone "
"file. This value is supplied in query responses to inform other servers how "
"long they should keep the data in cache. The default value is <tt>%s</tt>."
msgstr ""
"El valor mínimo de tiempo de vida se aplica a todos los registros de recursos en la zona"
"Archivo. Este valor se suministra en las respuestas de consulta para informar a otros servidores cómo"
"Durante mucho tiempo deberían mantener los datos en caché. El valor predeterminado es <tt>%s </tt>"
#: models.py:118
msgid "top domain with one subdomain"
msgstr ""
#: models.py:119
#, python-format
msgid "top domain with %d subdomains"
msgstr ""
#: models.py:121
msgid "subdomain"
msgstr "Subdominio"
#: models.py:308
msgid "A (IPv4 address)"
msgstr ""
#: models.py:309
msgid "AAAA (IPv6 address)"
msgstr ""
#: models.py:329
msgid "domain"
msgstr "dominio"
#: models.py:330
msgid "TTL"
msgstr ""
#: models.py:331
#, python-format
msgid "Record TTL, defaults to %s"
msgstr "Registro TTL, por defecto %s"
#: models.py:333
msgid "type"
msgstr "tipo"
#: models.py:335
msgid "value"
msgstr "valor"
#: models.py:336
msgid "MX, NS and CNAME records sould end with a dot."
msgstr "Los registros MX, NS and CNAME han de acabar con punto"
#: serializers.py:36
msgid "Can not create subdomains of other users domains"
msgstr "No puede crear subdominios de los dominios de otros usuarios"
#: templates/admin/domains/domain/change_form.html:11
msgid "History"
msgstr "Historial"
#: templates/admin/domains/domain/change_form.html:13
msgid "View on site"
msgstr "Ver en el sitio"
#: templates/admin/domains/domain/view_zone.html:8
msgid "Home"
msgstr ""
#: validators.py:28
msgid "This domain name is not allowed"
msgstr "Este nombre de dominio no esta permitido"
#: validators.py:37
msgid "Not a valid domain name."
msgstr "No es nombre de dominio valido"
#: validators.py:46
#, python-format
msgid "%s is not an appropiate zone interval value"
msgstr "%s no es un valor de intervalo de zona apropiado"
#: validators.py:57
msgid ""
"Labels must start and end with a letter or digit, and have as interior "
"characters only letters, digits, and hyphen."
msgstr ""
"Las etiquetas deben comenzar y terminar con una letra o dígito, y tener en su interior"
"Caracteres solo letras, dígitos y guiones"
#: validators.py:61
msgid "Use a fully expanded domain name ending with a dot."
msgstr "Use un nombre de dominio completo que termine con un punto"
#: validators.py:64
msgid "Labels must be 63 characters or less."
msgstr "Las etiquetas deben ser de 63 caracteres o menos"
#: validators.py:68
msgid ""
"MX record format is 'priority domain.' tuple, with priority being a number."
msgstr ""
"El formato de registro MX es 'dominio prioritario'. Tuple, con la prioridad siendo un número "
#: validators.py:83 validators.py:95
#, python-format
msgid "%s is not an appropiate SRV record value"
msgstr "%s no es un valor de SRV apropiado"
#: validators.py:111
#, python-format
msgid ""
"%s is not an appropiate CAA record value, sintax: 0-255 issue|issuewild|"
"iodef \"domain|mailto:email\""
msgstr ""
"%s no es un valor de registro de CAA apropiado, sintaxis: 0-255 issue|issuewild|"
"iodef \"domain|mailto:email\""
#: validators.py:134
msgid ""
"This record value contains spaces, you must enclose the string in double "
"quotes; otherwise, individual words will be separately quoted and break up "
"the record into multiple parts."
msgstr ""
"Este valor de registro contiene espacios, debe encerrar la cadena en el doble"
"Comillas; de lo contrario, las palabras individuales serán citadas por separado y se romperán"
"El registro en múltiples partes"

View File

@ -1,80 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:27
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.contrib.domains.utils
import orchestra.contrib.domains.validators
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Domain',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name')),
('serial', models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')),
('account', models.ForeignKey(blank=True, help_text='Automatically selected for subdomains.', on_delete=django.db.models.deletion.CASCADE, related_name='domains', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('top', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain')),
],
),
migrations.CreateModel(
name='Record',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ttl', models.CharField(blank=True, help_text='Record TTL, defaults to 1h', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL')),
('type', models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF')], max_length=32, verbose_name='type')),
('value', models.CharField(help_text='MX, NS and CNAME records sould end with a dot.', max_length=1024, verbose_name='value')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='domains.Domain', verbose_name='domain')),
],
),
migrations.AlterField(
model_name='domain',
name='serial',
field=models.IntegerField(default=orchestra.contrib.domains.utils.generate_zone_serial, editable=False, help_text='A revision number that changes whenever this domain is updated.', verbose_name='serial'),
),
migrations.AddField(
model_name='domain',
name='expire',
field=models.CharField(blank=True, help_text='The time that a secondary server will keep trying to complete a zone transfer. If this time expires prior to a successful zone transfer, the secondary server will expire its zone file. This means the secondary will stop answering queries. The default value is <tt>4w</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='expire'),
),
migrations.AddField(
model_name='domain',
name='min_ttl',
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>1h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
),
migrations.AddField(
model_name='domain',
name='refresh',
field=models.CharField(blank=True, help_text="The time a secondary DNS server waits before querying the primary DNS server's SOA record to check for changes. When the refresh time expires, the secondary DNS server requests a copy of the current SOA record from the primary. The primary DNS server complies with this request. The secondary DNS server compares the serial number of the primary DNS server's current SOA record and the serial number in it's own SOA record. If they are different, the secondary DNS server will request a zone transfer from the primary DNS server. The default value is <tt>1d</tt>.", max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='refresh'),
),
migrations.AddField(
model_name='domain',
name='retry',
field=models.CharField(blank=True, help_text='The time a secondary server waits before retrying a failed zone transfer. Normally, the retry time is less than the refresh time. The default value is <tt>2h</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='retry'),
),
migrations.AlterField(
model_name='domain',
name='name',
field=models.CharField(db_index=True, help_text='Domain or subdomain name.', max_length=256, unique=True, validators=[orchestra.contrib.domains.validators.validate_domain_name, orchestra.contrib.domains.validators.validate_allowed_domain], verbose_name='name'),
),
migrations.AlterField(
model_name='domain',
name='top',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_set', to='domains.Domain', verbose_name='top domain'),
),
migrations.AddField(
model_name='domain',
name='dns2136_address_match_list',
field=models.CharField(blank=True, default='key pangea.key;', help_text="A bind-9 'address_match_list' that will be granted permission to perform dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.", max_length=80),
),
]

View File

@ -1,29 +0,0 @@
# Generated by Django 2.2.24 on 2024-07-11 12:25
from django.db import migrations, models
import orchestra.contrib.domains.validators
class Migration(migrations.Migration):
dependencies = [
('domains', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='domain',
name='min_ttl',
field=models.CharField(blank=True, help_text='The minimum time-to-live value applies to all resource records in the zone file. This value is supplied in query responses to inform other servers how long they should keep the data in cache. The default value is <tt>30m</tt>.', max_length=16, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='min TTL'),
),
migrations.AlterField(
model_name='record',
name='ttl',
field=models.CharField(blank=True, help_text='Record TTL, defaults to 30m', max_length=8, validators=[orchestra.contrib.domains.validators.validate_zone_interval], verbose_name='TTL'),
),
migrations.AlterField(
model_name='record',
name='type',
field=models.CharField(choices=[('MX', 'MX'), ('NS', 'NS'), ('CNAME', 'CNAME'), ('A', 'A (IPv4 address)'), ('AAAA', 'AAAA (IPv6 address)'), ('SRV', 'SRV'), ('TXT', 'TXT'), ('SPF', 'SPF'), ('CAA', 'CAA')], max_length=32, verbose_name='type'),
),
]

View File

@ -145,9 +145,8 @@ class Domain(models.Model):
else: else:
zone += subdomain.render_records() zone += subdomain.render_records()
###darmengo 2021-03-25 add autoconfig ###darmengo 2021-03-25 add autoconfig
# TODO: que se asigne esta ip automaticamente
if self.has_default_mx(): if self.has_default_mx():
zone += 'autoconfig.{}. 30m IN A 45.150.186.197\n'.format(self.name) zone += 'autoconfig.{}. 30m IN A 109.69.8.133\n'.format(self.name)
###END darmengo 2021-03-25 add autoconfig ###END darmengo 2021-03-25 add autoconfig
for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True): for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True):
zone += subdomain.render_records() zone += subdomain.render_records()
@ -299,7 +298,6 @@ class Record(models.Model):
TXT = 'TXT' TXT = 'TXT'
SPF = 'SPF' SPF = 'SPF'
SOA = 'SOA' SOA = 'SOA'
CAA = 'CAA'
TYPE_CHOICES = ( TYPE_CHOICES = (
(MX, "MX"), (MX, "MX"),
@ -310,7 +308,6 @@ class Record(models.Model):
(SRV, "SRV"), (SRV, "SRV"),
(TXT, "TXT"), (TXT, "TXT"),
(SPF, "SPF"), (SPF, "SPF"),
(CAA, "CAA"),
) )
VALIDATORS = { VALIDATORS = {
@ -323,7 +320,6 @@ class Record(models.Model):
SPF: (validate_ascii, validators.validate_quoted_record), SPF: (validate_ascii, validators.validate_quoted_record),
SRV: (validators.validate_srv_record,), SRV: (validators.validate_srv_record,),
SOA: (validators.validate_soa_record,), SOA: (validators.validate_soa_record,),
CAA: (validators.validate_caa_record,),
} }
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE) domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE)

View File

@ -53,7 +53,7 @@ def validate_zone_label(value):
Labels may not be all numbers, but may have a leading digit (e.g., 3com.com). Labels may not be all numbers, but may have a leading digit (e.g., 3com.com).
Labels must end and begin only with a letter or digit. See [RFC 1035] and [RFC 1123]. Labels must end and begin only with a letter or digit. See [RFC 1035] and [RFC 1123].
""" """
if not re.match(r'^[a-z0-9][\.\-0-9a-z_]*[\.0-9a-z]$', value): if not re.match(r'^[a-z0-9][\.\-0-9a-z]*[\.0-9a-z]$', value):
msg = _("Labels must start and end with a letter or digit, " msg = _("Labels must start and end with a letter or digit, "
"and have as interior characters only letters, digits, and hyphen.") "and have as interior characters only letters, digits, and hyphen.")
raise ValidationError(msg) raise ValidationError(msg)
@ -105,28 +105,6 @@ def validate_soa_record(value):
raise ValidationError(msg) raise ValidationError(msg)
def validate_caa_record(value):
# 0-255 issue|issuewild|iodef "domain|mailto:email"
# 0 issue "letsewncript.org"
msg = _("%s is not an appropiate CAA record value, sintax: 0-255 issue|issuewild|iodef \"domain|mailto:email\"") % value
values = value.split()
if len(values) != 3:
raise ValidationError(msg)
patron_flag = r'^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
patron_tag = r'^(issue|issuewild|iodef)$'
patron_value_domain = r'^"[a-zA-Z0-9-.]+\.[a-zA-Z]+\.?"$'
patron_value_mailto = r'^"mailto:[a-zA-Z0-9.]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"$'
flag = re.match(patron_flag, values[0])
tag = re.match(patron_tag, values[1])
if values[1] == 'iodef':
valor = re.match(patron_value_mailto, values[2])
else:
valor = re.match(patron_value_domain, values[2])
if not (flag and tag and valor):
raise ValidationError(msg)
def validate_quoted_record(value): def validate_quoted_record(value):
value = value.strip() value = value.strip()
if ' ' in value and (value[0] != '"' or value[-1] != '"'): if ' ' in value and (value[0] != '"' or value[-1] != '"'):

View File

@ -1,22 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBBBBBBBB
BBBBB BBBBBBBBBBBBBBBBBB
BBBBBBBB
BBBBB BBBBBBBBBBB
BB BBBBBBBB
XXXX XXXXXXXXXXXXXXXXXXXX
XX XXXXXXBBB BBBBBBBBBBBBBXX gettext(u'Home') XXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFFFFFFFFFFFXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXX gettext(u'History') XXXX
XXXXXXXX gettext(u'Edit')
XXXXXX
BBBB
BBBBB
BBBBBBBB

View File

@ -1,43 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBBBBBBBB BBBBBB
BBBBB BBBBBBBBBBB
XXXX XXXXXXXXXXXXXXXXXXXX
XX XXXXXXBBB BBBBBBBBBBBBBXX gettext(u'Home') XXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXX
XXXXXXXX XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBXXFFFFFFFFFFFFFFFFFFXXXX
XXXXXXXX gettext(u'History')
XXXXXX
BBBBBBBB
BBBBB BBBBBBB
XXXX XXXXXXXXXXXXXXXXXX
XXXX XXXXXXXXXXXXXXX
BB BBBBBBBBBBB
XXXXXX XXXXXXXXXXXXXXXXXXXX
XXXXXXX
XXXX
XXX XXXXXXXXXXXX gettext(u'Date/time') XXXXX
XXX XXXXXXXXXXXX gettext(u'User') XXXXX
XXX XXXXXXXXXXXX gettext(u'Action') XXXXX
XXXXX
XXXXXXXX
XXXXXXX
BBB BBBBBB BB BBBBBBBBBBB
XXXX
XXX XXXXXXXXXXXXFFFFFFFFFFFFFFFFFFFFFFXXXXX
XXXXBB BBBBBBBBBBBBBBBBBBBBBBBBB XXBBBBBXXXXX
XXXXBB BBBBBBBBBBBBBBBBBB BBB BBB BBBBBBBBBBBBBBBBBBBBB gettext(u'Added') BBBBBBBBB XX XXXXXXBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXX XXXXXBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXXXXXXXXXXXXX
XXXXX
BBBBBB
XXXXXXXX
XXXXXXXX
BBBB
XXX gettext(u"This object doesn't have a change history. It probably wasn't added via this admin site.") XXXX
BBBBB
XXXXXX
XXXXXX
BBBBBBBB

View File

@ -1,110 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:27
from __future__ import unicode_literals
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.utils.timezone import utc
import orchestra.models.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('author_name', models.CharField(blank=True, max_length=256, verbose_name='author name')),
('content', models.TextField(verbose_name='content')),
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to=settings.AUTH_USER_MODEL, verbose_name='author')),
],
options={
'get_latest_by': 'id',
},
),
migrations.CreateModel(
name='Queue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True, verbose_name='name')),
('verbose_name', models.CharField(blank=True, max_length=128, verbose_name='verbose_name')),
('default', models.BooleanField(default=False, verbose_name='default')),
('notify', orchestra.models.fields.MultiSelectField(blank=True, choices=[('SUPPORT', 'Support tickets'), ('ADMIN', 'Administrative'), ('BILLING', 'Billing'), ('TECH', 'Technical'), ('ADDS', 'Announcements'), ('EMERGENCY', 'Emergency contact')], default=('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY'), help_text='Contacts to notify by email', max_length=256, verbose_name='notify')),
],
),
migrations.CreateModel(
name='Ticket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creator_name', models.CharField(blank=True, max_length=256, verbose_name='creator name')),
('subject', models.CharField(max_length=256, verbose_name='subject')),
('description', models.TextField(verbose_name='description')),
('priority', models.CharField(choices=[('HIGH', 'High'), ('MEDIUM', 'Medium'), ('LOW', 'Low')], default='MEDIUM', max_length=32, verbose_name='priority')),
('state', models.CharField(choices=[('NEW', 'New'), ('IN_PROGRESS', 'In Progress'), ('RESOLVED', 'Resolved'), ('FEEDBACK', 'Feedback'), ('REJECTED', 'Rejected'), ('CLOSED', 'Closed')], default='NEW', max_length=32, verbose_name='state')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified')),
('cc', models.TextField(blank=True, help_text='emails to send a carbon copy to', verbose_name='CC')),
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
('queue', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='issues.Queue')),
],
options={
'ordering': ['-updated_at'],
},
),
migrations.CreateModel(
name='TicketTracker',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trackers', to='issues.Ticket', verbose_name='ticket')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_trackers', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
),
migrations.AddField(
model_name='message',
name='ticket',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='issues.Ticket', verbose_name='ticket'),
),
migrations.AlterUniqueTogether(
name='tickettracker',
unique_together=set([('ticket', 'user')]),
),
migrations.AlterField(
model_name='ticket',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created'),
),
migrations.RemoveField(
model_name='message',
name='created_on',
),
migrations.AddField(
model_name='message',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 20, 10, 27, 45, 766388, tzinfo=utc), verbose_name='created at'),
preserve_default=False,
),
migrations.AlterField(
model_name='ticket',
name='creator',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_created', to=settings.AUTH_USER_MODEL, verbose_name='created by'),
),
migrations.AlterField(
model_name='ticket',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets_owned', to=settings.AUTH_USER_MODEL, verbose_name='assigned to'),
),
migrations.AlterField(
model_name='ticket',
name='queue',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='issues.Queue'),
),
]

View File

@ -1,55 +0,0 @@
XXXXXXXXX XXXX XXXXXX XXXXXXXXXXXX XXXXX XXX XXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXX
XXXXX XXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX XX
XXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX XX
XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX
XXXXXX XXXXXXXXXXXXXXXX
XX X XXXXXXXXXXXX XXXXXXXX XXXXXXXXXXX XXXXXXXXXX XXXXX XXXXXXXXXXX XXXXXXX XXXXXX XXXXX X
XXXX X XXXXXXXXXXXX XXXXXXXX XXXXXXXXXXX XXXXXXXXXX XXXXX XXXXXX XXXXX X
XXXXX XX X XXXXXXXXXXXX XXXX X
XXXXX XX X XXXXXXXXXXXXXXX XXXX XXXXXXXXXXXXXXXXX XXXXXXXX XXXXXXX XXXX XXXXXXXXXXXXXXX XXXXXXXX
XXXXX XX XXXX X XXXXXXXXXX XXXXXX X
XXXXX XX XX X XXXXXXXXXX XXXXXX XXXXXXXXXXX XXXXX X
XXXXX XX XX X XXXXXXXXXX XXXXXX XXXXXXXXXXX XXXXX X
XXXXX XX XX X XXXXXXXXXX XXXXXX XXXXXXXXXXX XXXXX X
XXXXX XX XXXX X XXXXXXXXXXXXXXXXX XXXX XXXXXXXXXXXXXXX XXXX X
XXXXXXXX
XXXXXXX
XXXXXX
XXXXXXXXXXXX XXXXXX XXXXX XXXXXXXXXXXXXX
XXXXXX XXXXXXXXXXXXX
XXXXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX X XX XXXX XXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX
XXXXXXX XXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXX X XXXX XXXX X XX XXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXX XXXXXXXX XXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXX XX XXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXX XXXXXXXXXX XXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX XXXX XXXXXXXX XXXXXXXXXXXXXXXXXXXXX
XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXX XXXX XXXX XXX XXXX XXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXX XXXX XXXX XXXX XXXX XXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXX
XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXX XXXXX X XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXX XXXXX XXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX
XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX
XXXXXXXX
XXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXX XXXXXXXXX XX XXXXXXXX XXXXXXXXXXX
XXXX
XXXXXXX
XXXXXXX

View File

@ -57,14 +57,6 @@ class MailmanVirtualDomainController(ServiceController):
def commit(self): def commit(self):
context = self.get_context_files() context = self.get_context_files()
super(MailmanVirtualDomainController, self).commit() super(MailmanVirtualDomainController, self).commit()
self.append(textwrap.dedent("""
# Apply changes if needed
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
postmap %(virtual_alias_domains)s
systemctl reload postfix
fi
exit $exit_code""") % context
)
def get_context_files(self): def get_context_files(self):
return { return {
@ -109,7 +101,7 @@ class MailmanController(MailmanVirtualDomainController):
for suffix in self.address_suffixes: for suffix in self.address_suffixes:
context['suffix'] = suffix context['suffix'] = suffix
# Because mailman doesn't properly handle lists aliases we need virtual aliases # Because mailman doesn't properly handle lists aliases we need virtual aliases
if context['address_name'] != context['name'] or context['address_domain'] != settings.LISTS_DEFAULT_DOMAIN: if context['address_name'] != context['name']:
aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s@grups.pangea.org" % context) aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s@grups.pangea.org" % context)
return '\n'.join(aliases) return '\n'.join(aliases)
@ -173,18 +165,7 @@ class MailmanController(MailmanVirtualDomainController):
def commit(self): def commit(self):
context = self.get_context_files() pass
self.append(textwrap.dedent("""
# Apply changes if needed
if [[ $UPDATED_VIRTUAL_ALIAS == 1 ]]; then
postmap %(virtual_alias)s
fi
if [[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]]; then
systemctl reload postfix
fi
exit $exit_code""") % context
)
def get_context_files(self): def get_context_files(self):
return { return {
@ -218,7 +199,7 @@ class MailmanTraffic(ServiceMonitor):
model = 'lists.List' model = 'lists.List'
resource = ServiceMonitor.TRAFFIC resource = ServiceMonitor.TRAFFIC
verbose_name = _("Mailman traffic") verbose_name = _("Mailman traffic")
script_executable = '/usr/bin/python3' script_executable = '/usr/bin/python'
monthly_sum_old_values = True monthly_sum_old_values = True
doc_settings = (settings, doc_settings = (settings,
('LISTS_MAILMAN_POST_LOG_PATH',) ('LISTS_MAILMAN_POST_LOG_PATH',)
@ -229,10 +210,9 @@ class MailmanTraffic(ServiceMonitor):
context = { context = {
'postlogs': str((postlog, postlog+'.1')), 'postlogs': str((postlog, postlog+'.1')),
'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"), 'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
'default_domain': settings.LISTS_DEFAULT_DOMAIN,
} }
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
import re
import subprocess import subprocess
import sys import sys
from datetime import datetime from datetime import datetime
@ -263,6 +243,7 @@ class MailmanTraffic(ServiceMonitor):
'Nov': '11', 'Nov': '11',
'Dec': '12', 'Dec': '12',
}} }}
mailman_addr = re.compile(r'.*-(admin|bounces|confirm|join|leave|owner|request|subscribe|unsubscribe)@.*|mailman@.*')
def prepare(object_id, list_name, ini_date): def prepare(object_id, list_name, ini_date):
global lists global lists
@ -274,33 +255,39 @@ class MailmanTraffic(ServiceMonitor):
for postlog in postlogs: for postlog in postlogs:
try: try:
with open(postlog, 'r') as postlog: with open(postlog, 'r') as postlog:
recps_dict = {{}}
for line in postlog.readlines(): for line in postlog.readlines():
line = line.split() line = line.split()
if 'recips,' in line: if len(line) < 11:
__, __, __, __, __, id, __, __, list_name, __, recps = line[:11]
recps_dict[id] = recps
continue continue
if not 'bytes' in line: month, day, time, year, __, __, __, list_name, __, addr, size = line[:11]
continue
month, day, time, year, __, __, __, __, list_name, __, addr, size = line[:12]
try: try:
list_name = list_name.split('@')[0]
list = lists[list_name] list = lists[list_name]
except KeyError: except KeyError:
continue continue
else: else:
# discard mailman messages because of inconsistent POST logging
if mailman_addr.match(addr):
continue
date = year + months[month] + day + time.replace(':', '') date = year + months[month] + day + time.replace(':', '')
if list[0] < int(date) < end_date: if list[0] < int(date) < end_date:
if id in recps_dict: size = size[5:-1]
list[2] += int(size) * int(recps_dict[id]) try:
list[2] += int(size)
except ValueError:
# anonymized post
pass
except IOError as e: except IOError as e:
sys.stderr.write(str(e)+'\\n') sys.stderr.write(str(e)+'\\n')
for list_name, opts in lists.items(): for list_name, opts in lists.items():
__, object_id, size = opts __, object_id, size = opts
print(object_id, size) if size:
cmd = ' '.join(('list_members', list_name, '| wc -l'))
ps = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subscribers = ps.communicate()[0].strip()
size *= int(subscribers)
sys.stderr.write("%s %s*%s traffic*subscribers\\n" % (object_id, size, subscribers))
print object_id, size
""").format(**context) """).format(**context)
) )

View File

@ -1,117 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-06 10:23+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:28 admin.py:42
msgid "Address"
msgstr "Direccio"
#: admin.py:32
msgid "Admin"
msgstr "Administrador"
#: admin.py:44
#, python-format
msgid "Additional address besides the default &lt;name&gt;@%s"
msgstr "Adreça addicional a més del valor predeterminat &lt;name&gt;@%s"
#: backends.py:16
msgid "Mailman virtdomain-only"
msgstr ""
#: backends.py:201
msgid "Mailman traffic"
msgstr "Trànsit de Mailman"
#: backends.py:309
msgid "Mailman subscribers"
msgstr "Subscriptors Mailman"
#: filters.py:7
msgid "has custom address"
msgstr "té adreça personalitzada"
#: filters.py:12
msgid "True"
msgstr ""
#: filters.py:13
msgid "False"
msgstr ""
#: models.py:13
msgid "name"
msgstr "nom"
#: models.py:14
#, python-format
msgid "Default list address &lt;name&gt;@%s"
msgstr "Adreça de llista predeterminada &lt;name&gt;@%s"
#: models.py:15
msgid "address name"
msgstr "Nom de l'adreça"
#: models.py:18
msgid "address domain"
msgstr "Domini adicional"
#: models.py:19
msgid "admin email"
msgstr "Correu electrònic d'administració"
#: models.py:20
msgid "Administration email address"
msgstr "Adreça de correu electrònic d'administració"
#: models.py:21
msgid "Account"
msgstr "Compte"
#: models.py:24
msgid "active"
msgstr "actiu"
#: models.py:25
msgid ""
"Designates whether this account should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
"Designa si aquest compte ha de ser tractat com a actiu. Deselecteu això "
"En lloc de suprimir els comptes."
#: models.py:47
msgid "Domain should be selected for provided address name."
msgstr "El domini s'ha de seleccionar per al nom de l'adreça proporcionat."
#: serializers.py:20
msgid "Password"
msgstr "Contrasenya"
#: serializers.py:25
msgid ""
"Enter a valid password. This value may contain any ascii character except "
"for '/\"/\\/ characters."
msgstr ""
"Introduïu una contrasenya vàlida. Aquest valor pot contenir qualsevol personatge ASCII excepte"
"Per a '/\"/\\/caràcters. "
#: serializers.py:43
msgid "address_domains should should be provided when providing an addres_name"
msgstr "S'hauria de proporcionar address_domains en proporcionar un address_name"

View File

@ -1,117 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-06 10:03+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:28 admin.py:42
msgid "Address"
msgstr "Dirección"
#: admin.py:32
msgid "Admin"
msgstr "Administrador"
#: admin.py:44
#, python-format
msgid "Additional address besides the default &lt;name&gt;@%s"
msgstr "Dirección adicional además del valor predeterminado &lt;name&gt;@%s"
#: backends.py:16
msgid "Mailman virtdomain-only"
msgstr ""
#: backends.py:201
msgid "Mailman traffic"
msgstr "Tráfico Mailman"
#: backends.py:309
msgid "Mailman subscribers"
msgstr "Subscriptores Mailman"
#: filters.py:7
msgid "has custom address"
msgstr "tiene dirección personalizada"
#: filters.py:12
msgid "True"
msgstr ""
#: filters.py:13
msgid "False"
msgstr ""
#: models.py:13
msgid "name"
msgstr "nombre"
#: models.py:14
#, python-format
msgid "Default list address &lt;name&gt;@%s"
msgstr "dirección de lista por defecto &lt;name&gt;@%s"
#: models.py:15
msgid "address name"
msgstr "nombre de dirección"
#: models.py:18
msgid "address domain"
msgstr "dominio adicional"
#: models.py:19
msgid "admin email"
msgstr "correo administrador"
#: models.py:20
msgid "Administration email address"
msgstr "dirección correo administrador"
#: models.py:21
msgid "Account"
msgstr "Cuenta"
#: models.py:24
msgid "active"
msgstr "activo"
#: models.py:25
msgid ""
"Designates whether this account should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
"Designa si esta cuenta debe tratarse como activa. Deseleccionar esto "
"En lugar de eliminar cuentas."
#: models.py:47
msgid "Domain should be selected for provided address name."
msgstr "El dominio debe seleccionarse para el nombre de la dirección proporcionado."
#: serializers.py:20
msgid "Password"
msgstr "Contraseña"
#: serializers.py:25
msgid ""
"Enter a valid password. This value may contain any ascii character except "
"for '/\"/\\/ characters."
msgstr ""
"Ingrese una contraseña válida. Este valor puede contener cualquier carácter ASCII excepto"
"Para '/\"/\\/caracteres."
#: serializers.py:43
msgid "address_domains should should be provided when providing an addres_name"
msgstr "address_domains debe proporcionarse al proporcionar un addres_name"

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Generated by Django 2.2.28 on 2023-09-01 14:59
# Generated by Django 1.10.5 on 2021-04-22 11:27
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -10,8 +8,10 @@ import orchestra.core.validators
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
('domains', '0001_initial'), ('domains', '__first__'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
@ -20,46 +20,15 @@ class Migration(migrations.Migration):
name='List', name='List',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Default list address &lt;name&gt;@lists.orchestra.lan', max_length=128, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name')), ('name', models.CharField(help_text='Default list address &lt;name&gt;@grups.pangea.org', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
('address_name', models.CharField(blank=True, max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='address name')), ('address_name', models.CharField(blank=True, max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='address name')),
('admin_email', models.EmailField(help_text='Administration email address', max_length=254, verbose_name='admin email')), ('admin_email', models.EmailField(help_text='Administration email address', max_length=254, verbose_name='admin email')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')), ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('address_domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='domains.Domain', verbose_name='address domain')), ('address_domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='domains.Domain', verbose_name='address domain')),
], ],
), options={
migrations.AlterUniqueTogether( 'unique_together': {('address_name', 'address_domain')},
name='list', },
unique_together=set([('address_name', 'address_domain')]),
),
migrations.AlterField(
model_name='list',
name='address_domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='domains.Domain', verbose_name='address domain'),
),
migrations.AlterField(
model_name='list',
name='address_name',
field=models.CharField(blank=True, max_length=52, validators=[orchestra.core.validators.validate_name], verbose_name='address name'),
),
migrations.AlterField(
model_name='list',
name='name',
field=models.CharField(help_text='Default list address &lt;name&gt;@grups.pangea.org', max_length=52, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
),
migrations.AlterField(
model_name='list',
name='address_name',
field=models.CharField(blank=True, max_length=64, validators=[orchestra.core.validators.validate_name], verbose_name='address name'),
),
migrations.AlterField(
model_name='list',
name='name',
field=models.CharField(help_text='Default list address &lt;name&gt;@grups.pangea.org', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
),
migrations.AlterField(
model_name='list',
name='name',
field=models.CharField(help_text='Default list address &lt;name&gt;@lists.orchestra.lan', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
), ),
] ]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.2.24 on 2024-07-11 12:25
from django.db import migrations, models
import orchestra.core.validators
class Migration(migrations.Migration):
dependencies = [
('lists', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='list',
name='name',
field=models.CharField(help_text='Default list address &lt;name&gt;@grups.pangea.org', max_length=64, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name'),
),
]

View File

@ -47,7 +47,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
) )
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
'fields': ('account_link', 'name', 'password1', 'password2', 'filtering', 'ratelimit'), 'fields': ('account_link', 'name', 'password1', 'password2', 'filtering'),
}), }),
(_("Custom filtering"), { (_("Custom filtering"), {
'classes': ('collapse',), 'classes': ('collapse',),
@ -61,7 +61,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
) )
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('name', 'password', 'is_active', 'account_link', 'filtering', 'ratelimit'), 'fields': ('name', 'password', 'is_active', 'account_link', 'filtering'),
}), }),
(_("Custom filtering"), { (_("Custom filtering"), {
'classes': ('collapse',), 'classes': ('collapse',),

View File

@ -84,11 +84,6 @@ class UNIXUserMaildirController(SieveFilteringMixin, ServiceController):
--home %(home)s \\ --home %(home)s \\
--password '%(password)s' --password '%(password)s'
fi fi
if [[ "%(is_active)s" == "True" ]]; then
usermod --unlock %(user)s
else
usermod --lock %(user)s
fi
mkdir -p %(home)s mkdir -p %(home)s
chmod 751 %(home)s chmod 751 %(home)s
chown %(user)s:%(group)s %(home)s""") % context chown %(user)s:%(group)s %(home)s""") % context
@ -151,13 +146,12 @@ class UNIXUserMaildirController(SieveFilteringMixin, ServiceController):
'user': mailbox.name, 'user': mailbox.name,
'group': mailbox.name, 'group': mailbox.name,
'name': mailbox.name, 'name': mailbox.name,
'password': mailbox.password, 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
'home': mailbox.get_home(), 'home': mailbox.get_home(),
'maildir': os.path.join(mailbox.get_home(), 'Maildir'), 'maildir': os.path.join(mailbox.get_home(), 'Maildir'),
'initial_shell': self.SHELL, 'initial_shell': self.SHELL,
'banner': self.get_banner(), 'banner': self.get_banner(),
'changepass': changepass, 'changepass': changepass,
'is_active': mailbox.active,
} }
context['deleted_home'] = settings.MAILBOXES_MOVE_ON_DELETE_PATH % context context['deleted_home'] = settings.MAILBOXES_MOVE_ON_DELETE_PATH % context
return context return context
@ -458,30 +452,12 @@ class DovecotMaildirDisk(ServiceMonitor):
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
function monitor () { function monitor () {
SIZE=$(du -sb $1/Maildir/ 2> /dev/null || echo 0) && echo $SIZE | awk '{print $1}' SIZE=$(du -sb $1/Maildir/ 2> /dev/null || echo 0) && echo $SIZE | awk '{print $1}'
list=()
}""")) }"""))
def monitor(self, mailbox): def monitor(self, mailbox):
context = self.get_context(mailbox) context = self.get_context(mailbox)
# self.append("echo %(object_id)s $(monitor %(maildir_path)s)" % context) # self.append("echo %(object_id)s $(monitor %(maildir_path)s)" % context)
# self.append("echo %(object_id)s $(monitor %(home)s)" % context) self.append("echo %(object_id)s $(monitor %(home)s)" % context)
self.append("list[${#list[@]}]=\'echo %(object_id)s $(monitor %(home)s)\'" % context)
def commit(self):
self.append(textwrap.dedent("""\
proces=0
for cmd in "${list[@]}"
do
eval $cmd &
proces=$((proces+1))
if [ $proces -ge 10 ];then
wait
proces=0
fi
done
wait
exit $exit_code
"""))
def get_context(self, mailbox): def get_context(self, mailbox):
context = { context = {
@ -500,7 +476,7 @@ class PostfixMailscannerTraffic(ServiceMonitor):
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
resource = ServiceMonitor.TRAFFIC resource = ServiceMonitor.TRAFFIC
verbose_name = _("Postfix-Mailscanner traffic") verbose_name = _("Postfix-Mailscanner traffic")
script_executable = '/usr/bin/python3' script_executable = '/usr/bin/python'
monthly_sum_old_values = True monthly_sum_old_values = True
doc_settings = (settings, doc_settings = (settings,
('MAILBOXES_MAIL_LOG_PATH',) ('MAILBOXES_MAIL_LOG_PATH',)
@ -530,10 +506,6 @@ class PostfixMailscannerTraffic(ServiceMonitor):
end_date = int(end_datetime.strftime('%Y%m%d%H%M%S')) end_date = int(end_datetime.strftime('%Y%m%d%H%M%S'))
months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
months = dict((m, '%02d' % n) for n, m in enumerate(months, 1)) months = dict((m, '%02d' % n) for n, m in enumerate(months, 1))
users = {{}}
sends = {{}}
register_imap_traffic = False
register_pop_traffic = False
def inside_period(month, day, time, ini_date): def inside_period(month, day, time, ini_date):
global months global months
@ -549,109 +521,91 @@ class PostfixMailscannerTraffic(ServiceMonitor):
date += time.replace(':', '') date += time.replace(':', '')
return ini_date < int(date) < end_date return ini_date < int(date) < end_date
def search_username(pattern, users, line): users = {{}}
match = pattern.search(line) delivers = {{}}
if not match: reverse = {{}}
return None
username = match.groups(1)[0]
if username not in users.keys():
return None
return username
def search_size(line, users, username, pattern):
month, day, time, req_id = line.split()[:4]
if inside_period(month, day, time, users[username][0]):
group = req_id.split('<')[-1][:-2]
matches = pattern.search(line)
if not matches:
return None, None
return group, matches
return None, None
def prepare(object_id, mailbox, ini_date): def prepare(object_id, mailbox, ini_date):
global users global users
global sends global delivers
global reverse
ini_date = to_local_timezone(ini_date) ini_date = to_local_timezone(ini_date)
ini_date = int(ini_date.strftime('%Y%m%d%H%M%S')) ini_date = int(ini_date.strftime('%Y%m%d%H%M%S'))
users[mailbox] = (ini_date, object_id) users[mailbox] = (ini_date, object_id)
sends[mailbox] = {{}} delivers[mailbox] = set()
reverse[mailbox] = set()
def monitor(users, sends, maillogs):
grupos = []
sasl_username_pattern = re.compile(r'sasl_username=([a-zA-Z0-9\.\-_]+)')
size_pattern = re.compile(r'size=(\d+),')
pop_username_pattern = re.compile(r' pop3\(([^)].*)\)')
pop_size_pattern = re.compile(r'size=(\d+)')
imap_username_pattern = re.compile(r' imap\(([^)].*)\)')
imap_size_pattern = re.compile(r"in=(\d+) out=(\d+)")
def monitor(users, delivers, reverse, maillogs):
targets = {{}}
counter = {{}}
user_regex = re.compile(r'\(Authenticated sender: ([^ ]+)\)')
for maillog in maillogs: for maillog in maillogs:
try: try:
with open(maillog, 'r') as maillog: with open(maillog, 'r') as maillog:
for line in maillog.readlines(): for line in maillog.readlines():
# Only search for Authenticated sendings # Only search for Authenticated sendings
if 'sasl_username=' in line: if '(Authenticated sender: ' in line:
# si el usuario es uno de los elegidos y el rango de tiempo es correcto username = user_regex.search(line).groups()[0]
# recoge el id de grupo try:
username = search_username(sasl_username_pattern, users, line) sender = users[username]
if username is None: except KeyError:
continue continue
month, day, time, __, __, req_id = line.split()[:6]
if inside_period(month, day, time, users[username][0]):
group = req_id[:-1]
sends[username][group] = 0
grupos.append(group)
else: else:
# busca el size de envios donde se alla anadido el groupID anteriormente, month, day, time, __, proc, id = line.split()[:6]
# una vez encontrado borra el groupID if inside_period(month, day, time, sender[0]):
for id in grupos: # Add new email
if id in line: delivers[id[:-1]] = username
match = size_pattern.search(line) # Look for a MailScanner requeue ID
if not match: elif ' Requeue: ' in line:
id, __, req_id = line.split()[6:9]
id = id.split('.')[0]
try:
username = delivers[id]
except KeyError:
pass
else:
targets[req_id] = (username, 0)
reverse[username].add(req_id)
# Look for the mail size and count the number of recipients of each email
else:
try:
month, day, time, __, proc, req_id, __, msize = line.split()[:8]
except ValueError:
# not interested in this line
continue continue
for k, v in sends.items(): if proc.startswith('postfix/'):
if id in sends[k].keys(): req_id = req_id[:-1]
sends[k][id] += int(match.groups(1)[0]) if msize.startswith('size='):
grupos.remove(id) try:
target = targets[req_id]
# pop trafic except KeyError:
if register_pop_traffic: pass
if 'pop3(' in line and 'size' in line: else:
username = search_username(pop_username_pattern, users, line) targets[req_id] = (target[0], int(msize[5:-1]))
if username is None: elif proc.startswith('postfix/smtp'):
continue try:
group, matches = search_size(line, users, username, pop_size_pattern) target = targets[req_id]
if group is not None and matches is not None : except KeyError:
sends[username][group] = int(matches.groups(1)[0]) pass
else:
# imap trafic if inside_period(month, day, time, users[target[0]][0]):
if register_imap_traffic: try:
if 'imap(' in line and 'out=' in line: counter[req_id] += 1
username = search_username(imap_username_pattern, users, line) except KeyError:
if username is None: counter[req_id] = 1
continue
group, matches = search_size(line, users, username, imap_size_pattern)
if group is not None and matches is not None :
value = int(matches.group(1)) + int(matches.group(2))
sends[username][group] = value
except IOError as e: except IOError as e:
sys.stderr.write(str(e)+'\\n') sys.stderr.write(str(e)+'\\n')
# devolver la sumatoria de valores a orchestra (id_user, size) for username, opts in users.iteritems():
for username, opts in users.items(): size = 0
total_size = 0 for req_id in reverse[username]:
for size in sends[username].values(): size += targets[req_id][1] * counter.get(req_id, 0)
total_size += size print opts[1], size
print(f"{{opts[1]}} {{total_size}}")
""").format(**context) """).format(**context)
) )
def commit(self): def commit(self):
self.append('monitor(users, sends, maillogs)') self.append('monitor(users, delivers, reverse, maillogs)')
def monitor(self, mailbox): def monitor(self, mailbox):
context = self.get_context(mailbox) context = self.get_context(mailbox)
@ -672,57 +626,3 @@ class RoundcubeIdentityController(ServiceController):
verbose_name = _("Roundcube Identity Controller") verbose_name = _("Roundcube Identity Controller")
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
class RSpamdRatelimitController(ServiceController):
"""
rspamd ratelimit to user
"""
verbose_name = _("rspamd ratelimit user")
model = 'mailboxes.Mailbox'
def save(self, mailbox):
context = self.get_context(mailbox)
self.append(textwrap.dedent("""
sed -i '/^%(user)s$/d' %(maps)s
echo '%(user)s' >> %(path_maps)s%(ratelimit)s.map
systemctl reload rspamd.service
""") % context
)
def delete(self, mailbox):
context = self.get_context(mailbox)
self.append(textwrap.dedent("""
sed -i '/^%(user)s$/d' %(maps)s
systemctl reload rspamd.service
""") % context
)
# def commit(self):
# self.append('[[ $RELOAD_RSPAMD -eq 1 ]] && systemctl reload rspamd.service')
# super().commit()
def get_context(self, mailbox):
maps = self.extract_group_maps()
context = {
'user': mailbox.name,
'ratelimit': mailbox.ratelimit,
'maps': maps,
'path_maps': settings.MAILBOXES_RATELIMIT_PATH_MAPS,
}
return context
def extract_group_maps(self):
"""
debulve string de todos los ficheros de maps assignados en settings para ratelimit
return string
"""
choice_groups = settings.MAILBOXES_RATELIMIT_GROUP
path = settings.MAILBOXES_RATELIMIT_PATH_MAPS
group_maps = ''
if len(choice_groups) > 0:
for choice in choice_groups:
group_maps += f"{path}{choice[0]}.map "
return group_maps

View File

@ -54,11 +54,6 @@ class MailboxForm(forms.ModelForm):
raise ValidationError("Name length should be less than %i." % max_length) raise ValidationError("Name length should be less than %i." % max_length)
return name return name
def clean_ratelimit(self):
ratelimit = self.cleaned_data['ratelimit']
if ratelimit is None:
ratelimit = settings.MAILBOXES_RATELIMIT_GROUP_DEFAULT
return ratelimit
class MailboxChangeForm(UserChangeForm, MailboxForm): class MailboxChangeForm(UserChangeForm, MailboxForm):
pass pass

View File

@ -1,298 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-06 09:36+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:31
msgid "autoresponse"
msgstr "autoresposta"
#: admin.py:52 admin.py:66 settings.py:177
msgid "Custom filtering"
msgstr "Filtre personalitzat"
#: admin.py:54
msgid ""
"Please remember to select <tt>custom filtering</tt> if you want this filter "
"to be applied."
msgstr ""
"Recordeu que seleccioneu <tt> Filtratge personalitzat </tt> si voleu aquest filtre"
"Per aplicar-se."
#: admin.py:58 admin.py:70 admin.py:114
msgid "Addresses"
msgstr "Adreces"
#: admin.py:122
msgid "Forward from"
msgstr ""
#: admin.py:127
msgid "Filtering"
msgstr ""
#: admin.py:181
#, python-format
msgid ""
"Mailbox '%s' local address matches '%s', please consider if selecting it "
"makes sense."
msgstr ""
#: admin.py:199
#, python-brace-format
msgid ""
"Address <a href='{url}'>{addr}</a> clashes with '{mailbox}' mailbox local "
"address. Consider adding this mailbox to the address."
msgstr ""
"Adreça <a href='{url}'> {addr} </a> xoca amb '{mailbox}' bustia local"
"Adreça. Penseu en afegir aquesta bústia a l'adreça."
#: admin.py:210
#, python-format
msgid ""
"You have provided a custom filtering but filtering selected option is %s"
msgstr ""
"Heu proporcionat un filtratge personalitzat, però l'opció seleccionada per filtrar és %s"
#: admin.py:246 admin.py:252
msgid "Email"
msgstr ""
#: admin.py:260
msgid "Mailboxes"
msgstr "Busties"
#: admin.py:269
msgid "Mailboxes links"
msgstr "Links de busties"
#: admin.py:282
msgid "Forward"
msgstr "Redireccio"
#: admin.py:321
#, python-format
msgid ""
"Address '%s' matches mailbox '%s' local address, please consider if makes "
"sense adding the mailbox on the mailboxes or forward field."
msgstr ""
#: backends.py:64
msgid "UNIX maildir user"
msgstr ""
#: backends.py:263
msgid "Postfix address virtdomain-only"
msgstr ""
#: backends.py:347
msgid "Postfix address"
msgstr ""
#: backends.py:434
msgid "Mail autoresponse"
msgstr ""
#: backends.py:445
msgid "Dovecot Maildir size"
msgstr ""
#: backends.py:502
msgid "Postfix-Mailscanner traffic"
msgstr ""
#: backends.py:672
msgid "Roundcube Identity Controller"
msgstr ""
#: backends.py:682
msgid "rspamd ratelimit user"
msgstr ""
#: filters.py:9
msgid "has mailbox"
msgstr ""
#: filters.py:14
msgid "True"
msgstr ""
#: filters.py:15
msgid "False"
msgstr ""
#: filters.py:28
msgid "has forward"
msgstr ""
#: filters.py:41
msgid "has address"
msgstr ""
#: filters.py:53
msgid "has type server"
msgstr "té un servidor de tipus"
#: forms.py:20 models.py:115
msgid "addresses"
msgstr "adreçes"
#: forms.py:84
msgid "Mailboxes or forward address should be provided."
msgstr "S'han de proporcionar bústies o adreça de reenviament."
#: models.py:17 models.py:101
msgid "name"
msgstr "nom"
#: models.py:19
#, python-format
msgid "Required. %s characters or fewer. Letters, digits and ./-/_ only."
msgstr "Obligatori. %s caràcters o menys. Cartes, dígits i ./-/_ només."
#: models.py:22
msgid "Enter a valid mailbox name."
msgstr "Introduïu un nom de bústia vàlid."
#: models.py:24
msgid "password"
msgstr "contrasenya"
#: models.py:25
msgid "account"
msgstr "compte"
#: models.py:30
msgid "filtering"
msgstr "filtre"
#: models.py:32
msgid ""
"Arbitrary email filtering in <a href='https://tty1.net/blog/2011/sieve-"
"tutorial_en.html'>sieve language</a>. This overrides any automatic junk "
"email filtering"
msgstr ""
"Filtratge de correu electrònic arbitrari a <a href='https://tty1.net/blog/2011/sieve-"
"Tutorial_en.html'> Idioma tamís </a>. Això substitueix qualsevol brossa automàtica"
"Filtratge de correu electrònic"
#: models.py:35
msgid "active"
msgstr "actiu"
#: models.py:36
msgid "ratelimit"
msgstr ""
#: models.py:43 models.py:106
msgid "mailboxes"
msgstr "busties"
#: models.py:76
msgid "Custom filtering is selected but not provided."
msgstr "El filtratge personalitzat està seleccionat però no es proporciona."
#: models.py:103
msgid "Address name, left blank for a <i>catch-all</i> address"
msgstr "Nom de l'adreça, deixa en blanc per a una adreça <i>aleatoria</i>"
#: models.py:105
msgid "domain"
msgstr "domini"
#: models.py:108
msgid "forward"
msgstr "redireccio"
#: models.py:110
msgid "Space separated email addresses or mailboxes"
msgstr "Adreces de correu electrònic o bústies de correu electrònic separades per l'espai"
#: models.py:111
msgid "Account"
msgstr "Compte"
#: models.py:147
#, python-format
msgid "Please use mailboxes field for '%s' mailbox."
msgstr "Utilitzeu el camp de bústies de correu per la bústia '%s'."
#: models.py:153
#, python-format
msgid "'%s' forwards to itself."
msgstr ""
#: models.py:175
msgid "address"
msgstr "direccio"
#: models.py:178
msgid "subject"
msgstr "subjecte"
#: models.py:179
msgid "message"
msgstr "missatge"
#: models.py:180
msgid "enabled"
msgstr "habilitat"
#: settings.py:25
msgid "Limit for system user based mailbox on Linux is 32."
msgstr "El límit per a la bústia basada en l'usuari del sistema a Linux és 32."
#: settings.py:98
msgid "Disable"
msgstr "deshabilitat"
#: settings.py:99
msgid "Reject spam (Score&ge;8)"
msgstr ""
#: settings.py:118
msgid "Reject spam (Score&ge;5)"
msgstr ""
#: settings.py:137
msgid "Archive spam (Score&ge;8)"
msgstr ""
#: settings.py:157
msgid "Archive spam (Score&ge;5)"
msgstr ""
#: validators.py:15
#, python-format
msgid "'%s' is not a correct email name."
msgstr "'%s' no és un nom de correu electrònic correcte."
#: validators.py:33
#, python-format
msgid "'%s' is already present."
msgstr "'%s' ja està present."
#: validators.py:41
#, python-format
msgid "'%s' is not a valid email address."
msgstr "'%s' no és una adreça de correu electrònic vàlida."
#: validators.py:45
#, python-format
msgid "'%s' is not an existent mailbox."
msgstr "'%s' no és una bústia existent."

View File

@ -1,305 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-06 09:35+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:31
msgid "autoresponse"
msgstr "autorespuesta"
#: admin.py:52 admin.py:66 settings.py:177
msgid "Custom filtering"
msgstr "Filtro personalizado"
#: admin.py:54
msgid ""
"Please remember to select <tt>custom filtering</tt> if you want this filter "
"to be applied."
msgstr ""
"Recuerde seleccionar <TT> Filtrado personalizado </tt> Si desea este "
"filtroAplicar"
#: admin.py:58 admin.py:70 admin.py:114
msgid "Addresses"
msgstr "Direcciones de coreo"
#: admin.py:122
msgid "Forward from"
msgstr ""
#: admin.py:127
msgid "Filtering"
msgstr "Filtración"
#: admin.py:181
#, python-format
msgid ""
"Mailbox '%s' local address matches '%s', please consider if selecting it "
"makes sense."
msgstr ""
"Mailbox '%s' coincidencia de dirección local '%s', considere si "
"seleccionarloTiene sentido"
#: admin.py:199
#, python-brace-format
msgid ""
"Address <a href='{url}'>{addr}</a> clashes with '{mailbox}' mailbox local "
"address. Consider adding this mailbox to the address."
msgstr ""
"Dirección <a href='{url}'>{addr}</a> se enfrenta con '{mailbox}' buzón "
"localDirección. Considere agregar este buzón a la dirección."
#: admin.py:210
#, python-format
msgid ""
"You have provided a custom filtering but filtering selected option is %s"
msgstr ""
"Ha proporcionado un filtrado personalizado, pero la opción de filtrado "
"seleccionada es %s"
#: admin.py:246 admin.py:252
msgid "Email"
msgstr ""
#: admin.py:260
msgid "Mailboxes"
msgstr ""
#: admin.py:269
msgid "Mailboxes links"
msgstr ""
#: admin.py:282
msgid "Forward"
msgstr "Redirección"
#: admin.py:321
#, python-format
msgid ""
"Address '%s' matches mailbox '%s' local address, please consider if makes "
"sense adding the mailbox on the mailboxes or forward field."
msgstr ""
"Dirección '%s' coincide con buzón '%s', considere si lo hacetiene sentido "
"agregar el buzón en los buzones o el campo Reenviar"
#: backends.py:64
msgid "UNIX maildir user"
msgstr ""
#: backends.py:263
msgid "Postfix address virtdomain-only"
msgstr ""
#: backends.py:347
msgid "Postfix address"
msgstr ""
#: backends.py:434
msgid "Mail autoresponse"
msgstr ""
#: backends.py:445
msgid "Dovecot Maildir size"
msgstr ""
#: backends.py:502
msgid "Postfix-Mailscanner traffic"
msgstr ""
#: backends.py:672
msgid "Roundcube Identity Controller"
msgstr ""
#: backends.py:682
msgid "rspamd ratelimit user"
msgstr ""
#: filters.py:9
msgid "has mailbox"
msgstr ""
#: filters.py:14
msgid "True"
msgstr ""
#: filters.py:15
msgid "False"
msgstr ""
#: filters.py:28
msgid "has forward"
msgstr ""
#: filters.py:41
msgid "has address"
msgstr ""
#: filters.py:53
msgid "has type server"
msgstr ""
#: forms.py:20 models.py:115
msgid "addresses"
msgstr "direcciones de correo"
#: forms.py:84
msgid "Mailboxes or forward address should be provided."
msgstr "Se deben proporcionar buzones o dirección de reenvío"
#: models.py:17 models.py:101
msgid "name"
msgstr "nombre"
#: models.py:19
#, python-format
msgid "Required. %s characters or fewer. Letters, digits and ./-/_ only."
msgstr "Requerido. %s caracteres o menos. Letras, dígitos y ./-/_ solamente"
#: models.py:22
msgid "Enter a valid mailbox name."
msgstr "Ingrese un nombre de buzón válido"
#: models.py:24
msgid "password"
msgstr "contraseña"
#: models.py:25
msgid "account"
msgstr "cuenta"
#: models.py:30
msgid "filtering"
msgstr "filtro"
#: models.py:32
msgid ""
"Arbitrary email filtering in <a href='https://tty1.net/blog/2011/sieve-"
"tutorial_en.html'>sieve language</a>. This overrides any automatic junk "
"email filtering"
msgstr ""
"Filtrado de correo electrónico arbitrario en <a href='https://tty1.net/"
"blog/2011/sieve-tutorial_en.html'>lenguaje de tamiz</a>. Esto anula "
"cualquier basura automáticaFiltrado por correo electrónico"
#: models.py:35
msgid "active"
msgstr "activo"
#: models.py:36
msgid "ratelimit"
msgstr ""
#: models.py:43 models.py:106
msgid "mailboxes"
msgstr "buzones"
#: models.py:76
msgid "Custom filtering is selected but not provided."
msgstr "El filtrado personalizado se selecciona pero no se proporciona"
#: models.py:103
msgid "Address name, left blank for a <i>catch-all</i> address"
msgstr ""
"Nombre de la dirección, dejado en blanco para dirección <i>aleatoria</i>"
#: models.py:105
msgid "domain"
msgstr "dominio"
#: models.py:108
msgid "forward"
msgstr "redirección"
#: models.py:110
msgid "Space separated email addresses or mailboxes"
msgstr "Direcciones de correo electrónico o buzones separados por un espacio"
#: models.py:111
msgid "Account"
msgstr "Cuenta"
#: models.py:147
#, python-format
msgid "Please use mailboxes field for '%s' mailbox."
msgstr "Utilice el campo de mailboxes para el buzón '%s'"
#: models.py:153
#, python-format
msgid "'%s' forwards to itself."
msgstr ""
#: models.py:175
msgid "address"
msgstr "dirección"
#: models.py:178
msgid "subject"
msgstr ""
#: models.py:179
msgid "message"
msgstr "mensage"
#: models.py:180
msgid "enabled"
msgstr "habilitado"
#: settings.py:25
msgid "Limit for system user based mailbox on Linux is 32."
msgstr ""
"El límite para el buzón basado en el usuario del sistema en Linux es 32."
#: settings.py:98
msgid "Disable"
msgstr "Deshabilitado"
#: settings.py:99
msgid "Reject spam (Score&ge;8)"
msgstr ""
#: settings.py:118
msgid "Reject spam (Score&ge;5)"
msgstr ""
#: settings.py:137
msgid "Archive spam (Score&ge;8)"
msgstr ""
#: settings.py:157
msgid "Archive spam (Score&ge;5)"
msgstr ""
#: validators.py:15
#, python-format
msgid "'%s' is not a correct email name."
msgstr "'%s' no es un nombre de email correcto."
#: validators.py:33
#, python-format
msgid "'%s' is already present."
msgstr ""
#: validators.py:41
#, python-format
msgid "'%s' is not a valid email address."
msgstr "'%s' no es una dirección valida."
#: validators.py:45
#, python-format
msgid "'%s' is not an existent mailbox."
msgstr "'%s' no es un mailbox existente."

View File

@ -1,67 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:27
from __future__ import unicode_literals
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import orchestra.contrib.mailboxes.validators
class Migration(migrations.Migration):
dependencies = [
('domains', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Address',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, help_text='Address name, left blank for a <i>catch-all</i> address', max_length=64, validators=[orchestra.contrib.mailboxes.validators.validate_emailname], verbose_name='name')),
('forward', models.CharField(blank=True, help_text='Space separated email addresses or mailboxes', max_length=256, validators=[orchestra.contrib.mailboxes.validators.validate_forward], verbose_name='forward')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='domains.Domain', verbose_name='domain')),
],
options={
'verbose_name_plural': 'addresses',
},
),
migrations.CreateModel(
name='Autoresponse',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=256, verbose_name='subject')),
('message', models.TextField(verbose_name='message')),
('enabled', models.BooleanField(default=False, verbose_name='enabled')),
('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='autoresponse', to='mailboxes.Address', verbose_name='address')),
],
),
migrations.CreateModel(
name='Mailbox',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Enter a valid mailbox name.')], verbose_name='name')),
('password', models.CharField(max_length=128, verbose_name='password')),
('filtering', models.CharField(choices=[('CUSTOM', 'Custom filtering'), ('DISABLE', 'Disable'), ('REDIRECT', 'Archive spam (Score&ge;8)'), ('REDIRECT5', 'Archive spam (Score&ge;5)'), ('REJECT', 'Reject spam (Score&ge;8)'), ('REJECT5', 'Reject spam (Score&ge;5)')], default='REDIRECT', max_length=16)),
('custom_filtering', models.TextField(blank=True, help_text="Arbitrary email filtering in <a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. This overrides any automatic junk email filtering", validators=[orchestra.contrib.mailboxes.validators.validate_sieve], verbose_name='filtering')),
('is_active', models.BooleanField(default=True, verbose_name='active')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mailboxes', to=settings.AUTH_USER_MODEL, verbose_name='account')),
],
options={
'verbose_name_plural': 'mailboxes',
},
),
migrations.AddField(
model_name='address',
name='mailboxes',
field=models.ManyToManyField(blank=True, related_name='addresses', to='mailboxes.Mailbox', verbose_name='mailboxes'),
),
migrations.AlterUniqueTogether(
name='address',
unique_together=set([('name', 'domain')]),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.24 on 2024-07-11 12:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailboxes', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='mailbox',
name='ratelimit',
field=models.CharField(blank=True, choices=[('userbase', 'base'), ('user100', '100'), ('user200', '200'), ('user500', '500')], default='userbase', max_length=100, null=True, verbose_name='ratelimit'),
),
]

View File

@ -33,11 +33,6 @@ class Mailbox(models.Model):
"<a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. " "<a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. "
"This overrides any automatic junk email filtering")) "This overrides any automatic junk email filtering"))
is_active = models.BooleanField(_("active"), default=True) is_active = models.BooleanField(_("active"), default=True)
ratelimit = models.CharField(_("ratelimit"),
max_length=100, null=True, blank=True,
choices=settings.MAILBOXES_RATELIMIT_GROUP,
default=settings.MAILBOXES_RATELIMIT_GROUP_DEFAULT,)
class Meta: class Meta:
verbose_name_plural = _("mailboxes") verbose_name_plural = _("mailboxes")

View File

@ -203,21 +203,3 @@ MAILBOXES_MOVE_ON_DELETE_PATH = Setting('MAILBOXES_MOVE_ON_DELETE_PATH',
help_text="Available fromat names: <tt>%s</tt>" % ', '.join(_backend_names), help_text="Available fromat names: <tt>%s</tt>" % ', '.join(_backend_names),
validators=[Setting.string_format_validator(_backend_names)], validators=[Setting.string_format_validator(_backend_names)],
) )
MAILBOXES_RATELIMIT_GROUP = Setting('MAILBOXES_RATELIMIT_GROUP', (
('userbase', 'base'),
('user100', '100'),
('user200', '200'),
('user500', '500'),
),
help_text="Available messages per second"
)
MAILBOXES_RATELIMIT_GROUP_DEFAULT = Setting('MAILBOXES_RATELIMIT_GROUP_DEFAULT',
'userbase',
choices=MAILBOXES_RATELIMIT_GROUP
)
MAILBOXES_RATELIMIT_PATH_MAPS = Setting('MAILBOXES_RATELIMIT_PATH_MAPS',
'/etc/rspamd/local.d/maps/',
)

View File

@ -1,85 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:28
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('state', models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failes')], default='QUEUED', max_length=16, verbose_name='State')),
('priority', models.PositiveIntegerField(choices=[(0, 'Critical (not queued)'), (1, 'High'), (2, 'Normal'), (3, 'Low')], default=2, verbose_name='Priority')),
('to_address', models.CharField(max_length=256)),
('from_address', models.CharField(max_length=256)),
('subject', models.CharField(max_length=256, verbose_name='subject')),
('content', models.TextField(verbose_name='content')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('retries', models.PositiveIntegerField(default=0, verbose_name='retries')),
('last_retry', models.DateTimeField(auto_now=True, verbose_name='last try')),
],
),
migrations.CreateModel(
name='SMTPLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('result', models.CharField(choices=[('SUCCESS', 'Success'), ('FAILURE', 'Failure')], default='SUCCESS', max_length=16)),
('date', models.DateTimeField(auto_now_add=True)),
('log_message', models.TextField()),
('message', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='mailer.Message')),
],
),
migrations.RenameField(
model_name='message',
old_name='last_retry',
new_name='last_try',
),
migrations.AlterField(
model_name='message',
name='last_try',
field=models.DateTimeField(verbose_name='last try'),
),
migrations.AlterField(
model_name='message',
name='subject',
field=models.TextField(verbose_name='subject'),
),
migrations.AlterField(
model_name='message',
name='last_try',
field=models.DateTimeField(null=True, verbose_name='last try'),
),
migrations.AlterField(
model_name='message',
name='state',
field=models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failed')], default='QUEUED', max_length=16, verbose_name='State'),
),
migrations.AlterField(
model_name='message',
name='last_try',
field=models.DateTimeField(db_index=True, null=True, verbose_name='last try'),
),
migrations.AlterField(
model_name='message',
name='priority',
field=models.PositiveIntegerField(choices=[(0, 'Critical (not queued)'), (1, 'High'), (2, 'Normal'), (3, 'Low')], db_index=True, default=2, verbose_name='Priority'),
),
migrations.AlterField(
model_name='message',
name='retries',
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='retries'),
),
migrations.AlterField(
model_name='message',
name='state',
field=models.CharField(choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failed')], db_index=True, default='QUEUED', max_length=16, verbose_name='State'),
),
]

View File

@ -1,14 +0,0 @@
BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBB
BBBB BBBB BBBBBBBBBB BBBBBB BBBBBBBBBB
BBBBB BBBBBBBBBBBBBBBBBB
XXXX
BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BB BBBBBBBBBBBBBBBB
XX XXXXXXBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBB BBBBBBBBX XXXXXXXXXXXXXXXXXXXX
gettext(u'Send pending') SSSS SSSSSSS
XXXX
XXXXX
BBBBBBBB

View File

@ -1 +0,0 @@
default_app_config = 'orchestra.contrib.apps.MetricsConfig'

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,7 +0,0 @@
from django.apps import AppConfig
class MetricsConfig(AppConfig):
name = 'orchestra.contrib.metrics'

View File

@ -1,99 +0,0 @@
from prometheus_client import start_http_server, Gauge
import random
from orchestra.contrib.accounts.models import Account
from orchestra.contrib.websites.models import Website
from orchestra.contrib.databases.models import Database
from orchestra.contrib.resources.models import ResourceData
from orchestra.contrib.mailboxes.models import Mailbox
from orchestra.contrib.lists.models import List
from orchestra.contrib.saas.models import SaaS
# Crear métricas de tipo Gauge con etiquetas
usuarios_metrica = Gauge('usuarios', 'Número total de usuarios', ['tipo', 'estado'])
websites_metrica = Gauge('websites_server', 'Número total de websites en server', ['target_server', 'estado'])
databases_metrica = Gauge('databases', 'Número total de websites en server', ['target_server'])
mailboxes_metrica = Gauge('mailbox', 'Número total de mailbox', ['estado'])
lists_metrica = Gauge('lists', 'Número total de listas')
saas_metrica = Gauge('saas', 'Número total de saas', ['service', 'estado'])
usuarios_top_size_metrica = Gauge('usuarios_top_size', 'Top 10 cuentas ocupan espacio', ['object'])
databases_top_size_metrica = Gauge('databases_top_size', 'Top 10 databases ocupan espacio', ['object'])
mailboxes_top_size_metrica = Gauge('mailboxes_top_size', 'Top 10 mailboxes ocupan espacio', ['object'])
saas_top_size_metrica = Gauge('saas_top_size', 'Top 10 saas ocupan espacio', ['object'])
def get_size_resourcedata(id_resource, metrica):
top_resources = ResourceData.objects.filter(resource_id=id_resource, used__isnull=False).order_by('-used')[:10]
for resourcedata in top_resources:
metrica.labels(object=resourcedata.content_object_repr).set(resourcedata.used)
def get_data_objects_with_active(model, field_type):
objects = model.objects.all()
object_dict = {}
for obj in objects:
key = getattr(obj, field_type)
if key not in object_dict:
object_dict[key] = {'activo':0, 'inactivo':0}
if getattr(obj, 'is_active'):
object_dict[key]['activo'] += 1
else:
object_dict[key]['inactivo'] += 1
return object_dict
def actualizar_metrica_usuarios():
get_size_resourcedata(4, usuarios_top_size_metrica)
user_dict = get_data_objects_with_active(Account, 'type')
# envia metrica por cada tipo de usuario num de activos y inactivos
for type, value in user_dict.items():
usuarios_metrica.labels(tipo=type, estado='activo').set(value['activo'])
usuarios_metrica.labels(tipo=type, estado='no_activo').set(value['inactivo'])
def actualizar_metrica_websites():
website_dict = get_data_objects_with_active(Website, 'target_server')
for server, value in website_dict.items():
websites_metrica.labels(target_server=server, estado='activo').set(value['activo'])
websites_metrica.labels(target_server=server, estado='no_activo').set(value['inactivo'])
def actualizar_metrica_databases():
get_size_resourcedata(5, databases_top_size_metrica)
databases = Database.objects.all()
data = {}
for database in databases:
if database.target_server.name not in data.keys():
data[database.target_server.name] = {'total':0}
data[database.target_server.name]['total'] += 1
for server, value in data.items():
databases_metrica.labels(target_server=server).set(value['total'])
def actualizar_metrica_mailboxes():
get_size_resourcedata(1, mailboxes_top_size_metrica)
mailboxes = Mailbox.objects.all()
mailbox_activos = sum(1 for mailbox in mailboxes if mailbox.is_active)
mailbox_inactivos = len(mailboxes) - mailbox_activos
mailboxes_metrica.labels(estado='activo').set(mailbox_activos)
mailboxes_metrica.labels(estado='no_activo').set(mailbox_inactivos)
def actualizar_metrica_lists():
lists = List.objects.all()
lists_metrica.set(len(lists))
def actualizar_metrica_saas():
get_size_resourcedata(23, saas_top_size_metrica)
saas_dict = get_data_objects_with_active(SaaS, 'service')
for servicio, value in saas_dict.items():
saas_metrica.labels(service=servicio, estado='activo').set(value['activo'])
saas_metrica.labels(service=servicio, estado='no_activo').set(value['inactivo'])

View File

@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,27 +0,0 @@
from django.shortcuts import render
from django.http import HttpResponse
from django.views.decorators.http import require_GET
from prometheus_client.exposition import generate_latest
from prometheus_client import REGISTRY, CONTENT_TYPE_LATEST
from .custom_metrics import (
actualizar_metrica_usuarios,
actualizar_metrica_websites,
actualizar_metrica_databases,
actualizar_metrica_mailboxes,
actualizar_metrica_lists,
actualizar_metrica_saas,
)
@require_GET
def metrics_view(request):
# Actualizar métricas antes de generar el contenido
actualizar_metrica_usuarios()
actualizar_metrica_websites()
actualizar_metrica_databases()
actualizar_metrica_mailboxes()
actualizar_metrica_lists()
actualizar_metrica_saas()
# Devolver las métricas exportadas como respuesta HTTP
output = generate_latest(REGISTRY)
return HttpResponse(output, content_type=CONTENT_TYPE_LATEST)

View File

@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2021-04-22 11:28
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import orchestra.core.validators
import orchestra.models.fields
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Miscellaneous',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', orchestra.models.fields.NullableCharField(help_text='A unique identifier for this service.', max_length=256, null=True, unique=True, verbose_name='identifier')),
('description', models.TextField(blank=True, verbose_name='description')),
('amount', models.PositiveIntegerField(default=1, verbose_name='amount')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this service should be treated as active. Unselect this instead of deleting services.', verbose_name='active')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='miscellaneous', to=settings.AUTH_USER_MODEL, verbose_name='account')),
],
options={
'verbose_name_plural': 'miscellaneous',
},
),
migrations.CreateModel(
name='MiscService',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Raw name used for internal referenciation, i.e. service match definition', max_length=32, unique=True, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
('verbose_name', models.CharField(blank=True, help_text='Human readable name', max_length=256, verbose_name='verbose name')),
('description', models.TextField(blank=True, help_text='Optional description', verbose_name='description')),
('has_identifier', models.BooleanField(default=True, help_text='Designates if this service has a <b>unique text</b> field that identifies it or not.', verbose_name='has identifier')),
('has_amount', models.BooleanField(default=False, help_text='Designates whether this service has <tt>amount</tt> property or not.', verbose_name='has amount')),
('is_active', models.BooleanField(default=True, help_text='Whether new instances of this service can be created or not. Unselect this instead of deleting services.', verbose_name='active')),
],
),
migrations.AddField(
model_name='miscellaneous',
name='service',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='miscellaneous.MiscService', verbose_name='service'),
),
migrations.AlterField(
model_name='miscellaneous',
name='identifier',
field=orchestra.models.fields.NullableCharField(db_index=True, help_text='A unique identifier for this service.', max_length=256, null=True, unique=True, verbose_name='identifier'),
),
]

View File

@ -1,260 +0,0 @@
import urllib.parse
from django.conf import settings
from django.contrib.auth import authenticate, login
from django.http import Http404
from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _
from orchestra.contrib.domains.models import Domain
from orchestra.contrib.mailboxes.models import Mailbox
from orchestra.contrib.websites.models import Website
from .models import Address, DatabaseService, SaasService, UserAccount
DOMAINS_PATH = 'domains/'
TOKEN_PATH = '/api-token-auth/'
API_PATHS = {
# auth
'token-auth': '/api-token-auth/',
'my-account': 'accounts/',
# services
'database-list': 'databases/',
'domain-list': 'domains/',
'domain-detail': 'domains/{pk}/',
'address-list': 'addresses/',
'address-detail': 'addresses/{pk}/',
'mailbox-list': 'mailboxes/',
'mailbox-detail': 'mailboxes/{pk}/',
'mailbox-password': 'mailboxes/{pk}/set_password/',
'mailinglist-list': 'lists/',
# 'saas-list': 'saas/',
'website-list': 'websites/',
# other
'bill-list': 'bills/',
'bill-document': 'bills/{pk}/document/',
'payment-source-list': 'payment-sources/',
}
class Orchestra(object):
def __init__(self, request, username=None, password=None, **kwargs):
self.request = request
self.username = username
self.user = self.authenticate(self.username, password)
def authenticate(self, username, password):
user = authenticate(self.request, username=username, password=password)
if user is not None:
login(self.request, user)
return user
# Return an 'invalid login' error message.
return None
class OrchestraConnector:
def __init__(self, request):
self._request = request
self.user = request.user
assert not self.user.is_anonymous
def build_absolute_uri(self, path_name):
path = API_PATHS.get(path_name, None)
if path is None:
raise NoReverseMatch(
"Not found API path name '{}'".format(path_name))
return urllib.parse.urljoin(self.base_url, path)
def request(self, verb, resource=None, url=None, data=None, render_as="json", querystring=None, raise_exception=True):
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
if resource is not None:
url = self.build_absolute_uri(resource)
elif url is None:
raise AttributeError("Provide `resource` or `url` params")
if querystring is not None:
url = "{}?{}".format(url, querystring)
verb = getattr(self.session, verb.lower())
headers = {
"Authorization": "Token {}".format(self.auth_token),
"Content-Type": "application/json",
}
response = verb(url, json=data, headers=headers, allow_redirects=False)
if raise_exception:
response.raise_for_status()
status = response.status_code
if status < 500 and render_as == "json":
output = response.json()
else:
output = response.content
return status, output
def retrieve_service_list(self, model_class, querystring=None):
qs = model_class.objects.filter(account=self.user)
# TODO filter by querystring
return qs
# pattern_name = '{}-list'.format(service_name)
# if pattern_name not in API_PATHS:
# raise ValueError("Unknown service {}".format(service_name))
# _, output = self.request("GET", pattern_name, querystring=querystring)
# return output
def retrieve_profile(self):
if self.user.is_anonymous:
raise PermissionError("Cannot retrieve profile of an anonymous user.")
return self.user # return UserAccount.new_from_json(output[0])
def retrieve_bill_document(self, pk):
path = API_PATHS.get('bill-document').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
status, bill_pdf = self.request("GET", render_as="html", url=url, raise_exception=False)
if status == 404:
raise Http404(_("No domain found matching the query"))
return bill_pdf
def create_mail_address(self, data):
resource = '{}-list'.format(Address.api_name)
return self.request("POST", resource=resource, data=data)
def retrieve_mail_address(self, pk):
path = API_PATHS.get('address-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
status, data = self.request("GET", url=url, raise_exception=False)
if status == 404:
raise Http404(_("No object found matching the query"))
return Address.new_from_json(data)
def update_mail_address(self, pk, data):
path = API_PATHS.get('address-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
return self.request("PUT", url=url, data=data)
def retrieve_mail_address_list(self, querystring=None):
# retrieve mails applying filters (if any)
raw_data = self.retrieve_service_list(
Address.api_name,
querystring=querystring,
)
addresses = [Address.new_from_json(data) for data in raw_data]
# PATCH to include Pangea addresses not shown by orchestra
# described on issue #4
# TODO(@slamora) disabled hacky patch because breaks another funtionalities
# XXX Fix it on orchestra instead of here???
# raw_mailboxes = self.retrieve_mailbox_list()
# for mailbox in raw_mailboxes:
# if mailbox['addresses'] == []:
# address_data = {
# 'names': [mailbox['name']],
# 'forward': '',
# 'domain': {
# 'name': 'pangea.org.',
# },
# 'mailboxes': [mailbox],
# }
# pangea_address = Address.new_from_json(address_data)
# addresses.append(pangea_address)
return addresses
def delete_mail_address(self, pk):
path = API_PATHS.get('address-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
return self.request("DELETE", url=url, render_as=None)
def create_mailbox(self, data):
resource = '{}-list'.format(Mailbox.api_name)
return self.request("POST", resource=resource, data=data, raise_exception=False)
def retrieve_mailbox(self, pk):
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
status, data_json = self.request("GET", url=url, raise_exception=False)
if status == 404:
raise Http404(_("No mailbox found matching the query"))
return Mailbox.new_from_json(data_json)
def update_mailbox(self, pk, data):
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
status, response = self.request("PATCH", url=url, data=data, raise_exception=False)
return status, response
def retrieve_mailbox_list(self):
qs = self.retrieve_service_list(Mailbox)
return qs
def delete_mailbox(self, pk):
path = API_PATHS.get('mailbox-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
# Mark as inactive instead of deleting
# return self.request("DELETE", url=url, render_as=None)
return self.request("PATCH", url=url, data={"is_active": False})
def set_password_mailbox(self, pk, data):
path = API_PATHS.get('mailbox-password').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
status, response = self.request("POST", url=url, data=data, raise_exception=False)
return status, response
def retrieve_domain(self, pk):
path = API_PATHS.get('domain-detail').format_map({'pk': pk})
url = urllib.parse.urljoin(self.base_url, path)
status, domain_json = self.request("GET", url=url, raise_exception=False)
if status == 404:
raise Http404(_("No domain found matching the query"))
return Domain.new_from_json(domain_json)
def retrieve_domain_list(self):
domains = self.retrieve_service_list(Domain)
domains = domains.prefetch_related("addresses", "websites")
# TODO(@slamora): update when backend provides resource disk usage data
# initialize domain usage for every domain
# for domain in domains:
# domain.usage = {
# 'usage': 300,
# 'total': 650,
# 'unit': 'MB',
# 'percent': 50,
# }
return domains
def retrieve_website_list(self):
qs = self.retrieve_service_list(Website)
return qs
def verify_credentials(self):
"""
Returns:
A user profile info if the
credentials are valid, None otherwise.
"""
status, output = self.request("GET", 'my-account', raise_exception=False)
if status < 400:
return output
return None

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class MusicianConfig(AppConfig):
name = 'orchestra.contrib.musician'

View File

@ -1,38 +0,0 @@
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
SESSION_KEY_TOKEN = '_auth_token'
SESSION_KEY_USERNAME = '_auth_username'
def login(request, username, token):
"""
Persist a user id and a backend in the request. This way a user doesn't
have to reauthenticate on every request. Note that data set during
the anonymous session is retained when the user logs in.
"""
if SESSION_KEY_TOKEN in request.session:
if request.session[SESSION_KEY_USERNAME] != username:
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
else:
request.session.cycle_key()
request.session[SESSION_KEY_TOKEN] = token
request.session[SESSION_KEY_USERNAME] = username
# if hasattr(request, 'user'):
# request.user = user
rotate_token(request)
def logout(request):
"""
Remove the authenticated user's ID from the request and flush their session
data.
"""
request.session.flush()
# if hasattr(request, 'user'):
# from django.contrib.auth.models import AnonymousUser
# request.user = AnonymousUser()

View File

@ -1,211 +0,0 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from orchestra.utils.python import random_ascii
from django.forms.widgets import HiddenInput
from django.contrib.auth.hashers import make_password
from orchestra.forms.widgets import SpanWidget
from orchestra.forms import widgets
from django.utils.safestring import mark_safe
from orchestra.contrib.domains.models import Domain, Record
from orchestra.contrib.mailboxes.models import Address, Mailbox
from orchestra.contrib.systemusers.models import WebappUsers, SystemUser
from orchestra.contrib.saas.models import SaaS
from orchestra.contrib.musician.validators import ValidateZoneMixin
from . import api
class LoginForm(AuthenticationForm):
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username is not None and password:
orchestra = api.Orchestra(self.request, username=username, password=password)
if orchestra.user is None:
raise self.get_invalid_login_error()
else:
self.username = username
self.user = orchestra.user
return self.cleaned_data
class ChangePasswordForm(forms.ModelForm):
error_messages = {
'password_mismatch': _('The two password fields didnt match.'),
}
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
strip=False,
help_text=_("Enter the same password as before, for verification."),
)
class Meta:
fields = ("password",)
model = WebappUsers
def clean_password2(self):
password = self.cleaned_data.get("password")
password2 = self.cleaned_data.get("password2")
if password and password2 and password != password2:
raise ValidationError(
self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
cleaned_data['password'] = make_password(password)
return cleaned_data
class MailForm(forms.ModelForm):
class Meta:
model = Address
fields = ("name", "domain", "mailboxes", "forward")
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['domain'].queryset = Domain.objects.filter(account=self.user)
self.fields['mailboxes'].queryset = Mailbox.objects.filter(account=self.user)
def clean(self):
cleaned_data = super().clean()
if not cleaned_data.get('mailboxes') and not cleaned_data.get('forward'):
raise ValidationError("A mailbox or forward address should be provided.")
return cleaned_data
def save(self, commit=True):
instance = super().save(commit=False)
instance.account = self.user
if commit:
super().save(commit=True)
return instance
class MailboxChangePasswordForm(ChangePasswordForm):
class Meta:
fields = ("password",)
model = Mailbox
class MailboxCreateForm(forms.ModelForm):
error_messages = {
'password_mismatch': _('The two password fields didnt match.'),
}
name = forms.CharField()
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
strip=False,
help_text=_("Enter the same password as before, for verification."),
)
addresses = forms.ModelMultipleChoiceField(queryset=Address.objects.none(), required=False)
class Meta:
fields = ("name", "password", "password2", "addresses")
model = Mailbox
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['addresses'].queryset = Address.objects.filter(account=user)
self.user = user
def clean_password2(self):
password = self.cleaned_data.get("password")
password2 = self.cleaned_data.get("password2")
if password and password2 and password != password2:
raise ValidationError(
self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
cleaned_data['password'] = make_password(password)
return cleaned_data
def save(self, commit=True):
instance = super().save(commit=False)
instance.account = self.user
if commit:
super().save(commit=True)
return instance
class MailboxUpdateForm(forms.ModelForm):
addresses = forms.MultipleChoiceField(required=False)
class Meta:
fields = ('addresses',)
model = Mailbox
class MailboxSearchForm(forms.Form):
name = forms.CharField(required=False)
address = forms.CharField(required=False)
class RecordCreateForm(ValidateZoneMixin, forms.ModelForm):
class Meta:
model = Record
fields = ("ttl", "type", "value")
def __init__(self, *args, **kwargs):
self.domain = kwargs.pop('domain')
super().__init__(*args, **kwargs)
def save(self, commit=True):
instance = super().save(commit=False)
instance.domain = self.domain
if commit:
super().save(commit=True)
return instance
class RecordUpdateForm(ValidateZoneMixin, forms.ModelForm):
class Meta:
model = Record
fields = ("ttl", "type", "value")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.domain = self.instance.domain
class WebappUsersChangePasswordForm(ChangePasswordForm):
class Meta:
fields = ("password",)
model = WebappUsers
class SystemUsersChangePasswordForm(ChangePasswordForm):
class Meta:
fields = ("password",)
model = SystemUser

View File

@ -1,38 +0,0 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from orchestra.contrib.lists.models import List
from orchestra.contrib.domains.models import Domain
class MailingUpdateForm(forms.ModelForm):
class Meta:
model = List
fields = ("is_active", "name", "address_name", "address_domain")
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
qs = Domain.objects.filter(account=self.user)
self.fields['address_domain'].queryset = qs
self.fields['address_name'].help_text = _("Additional address besides the default <name>@grups.pangea.org")
self.fields['name'].widget.attrs['readonly'] = True
class MailingCreateForm(forms.ModelForm):
class Meta:
model = List
fields = ("name", "address_name", "address_domain", "admin_email")
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
qs = Domain.objects.filter(account=self.user)
self.fields['address_domain'].queryset = qs
self.fields['address_name'].help_text = _("Additional address besides the default <name>@grups.pangea.org")
def save(self, commit=True):
instance = super().save(commit=False)
instance.account = self.user
if commit:
super().save(commit=True)
return instance

View File

@ -1,80 +0,0 @@
from django.utils.translation import gettext_lazy as _
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.views.generic.list import ListView
from orchestra.contrib.musician.mixins import (CustomContextMixin, ExtendedPaginationMixin,
UserTokenRequiredMixin)
from django.views.generic.edit import (CreateView, DeleteView, FormView,
UpdateView)
from orchestra.contrib.lists.models import List
from orchestra.contrib.domains.models import Domain, Record
from orchestra.contrib.lists.settings import LISTS_DEFAULT_DOMAIN
from .forms import MailingUpdateForm, MailingCreateForm
class MailingListsView(CustomContextMixin, UserTokenRequiredMixin, ListView):
model = List
template_name = "musician/mailinglist_list.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Mailing lists'),
}
def get_queryset(self):
return self.model.objects.filter(account=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
domain_id = self.request.GET.get('domain')
if domain_id:
qs = Domain.objects.filter(account=self.request.user)
context.update({
'active_domain': get_object_or_404(qs, pk=domain_id),
})
context.update({'default_domain': LISTS_DEFAULT_DOMAIN})
return context
def get_queryfilter(self):
"""Retrieve query params (if any) to filter queryset"""
domain_id = self.request.GET.get('domain')
if domain_id:
return {"address_domain_id": domain_id}
return {}
class MailingUpdateView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
model = List
form_class = MailingUpdateForm
template_name = "musician/mailinglist_form.html"
def get_queryset(self):
qs = List.objects.filter(account=self.request.user)
return qs
def get_success_url(self):
return reverse_lazy("musician:mailing-lists")
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
class MailingCreateView(CustomContextMixin, UserTokenRequiredMixin, CreateView):
model = List
form_class = MailingCreateForm
template_name = "musician/mailinglist_form.html"
success_url = reverse_lazy("musician:mailing-lists")
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
class MailingDeleteView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
template_name = "musician/mailing_check_delete.html"
model = List
success_url = reverse_lazy("musician:mailing-lists")
def get_queryset(self):
return self.model.objects.filter(account=self.request.user)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +0,0 @@
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.utils.translation import gettext_lazy as _
from django.views.generic.base import ContextMixin
from orchestra import get_version
from . import api
from .settings import LANGUAGES
from .auth import SESSION_KEY_TOKEN
class CustomContextMixin(ContextMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# generate services menu items
services_menu = [
{'icon': 'fas fa-home', 'pattern_name': 'musician:dashboard', 'title': _('Dashboard')},
{'icon': 'fas fa-globe', 'pattern_name': 'musician:domain-list', 'title': _('Domains')},
{'icon': 'fas fa-envelope', 'pattern_name': 'musician:address-list', 'title': _('Mails')},
{'icon': 'fas fa-mail-bulk', 'pattern_name': 'musician:mailing-lists', 'title': _('Mailing lists')},
{'icon': 'fas fa-database', 'pattern_name': 'musician:database-list', 'title': _('Databases')},
{'icon': 'fas fa-fire', 'pattern_name': 'musician:saas-nextcloud-list', 'title': _('SaaS')},
{'icon': 'fas fa-cloud', 'pattern_name': 'musician:saas-nextcloud-list', 'title': _('Nextcloud'), 'indent': True},
{'icon': 'fab fa-wordpress', 'pattern_name': 'musician:saas-wordpress-list', 'title': _('Community WP'), 'indent': True},
{'icon': 'fas fa-globe', 'pattern_name': 'musician:website-list', 'title': _('Websites')},
{'icon': 'fas fa-folder', 'pattern_name': 'musician:webapp-list', 'title': _('Webapps'), 'indent': True},
{'icon': 'fas fa-user', 'pattern_name': 'musician:systemuser-list', 'title': _('Users'), 'indent': True},
]
context.update({
'services_menu': services_menu,
'version': get_version(),
# 'languages': settings.LANGUAGES,
'languages': LANGUAGES,
})
return context
class ExtendedPaginationMixin:
paginate_by = 20
paginate_by_kwarg = 'per_page'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'per_page_values': [5, 10, 20, 50],
'per_page_param': self.paginate_by_kwarg,
})
return context
def get_paginate_by(self, queryset):
per_page = self.request.GET.get(self.paginate_by_kwarg) or self.paginate_by
try:
paginate_by = int(per_page)
except ValueError:
paginate_by = self.paginate_by
return paginate_by
class UserTokenRequiredMixin(LoginRequiredMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
self.orchestra = api.OrchestraConnector(self.request)
context.update({
'profile': self.orchestra.retrieve_profile(),
})
return context

View File

@ -1,401 +0,0 @@
import ast
import logging
from django.utils.dateparse import parse_datetime
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from . import settings as musician_settings
from .utils import get_bootstraped_percent
logger = logging.getLogger(__name__)
class OrchestraModel:
""" Base class from which all orchestra models will inherit. """
api_name = None
verbose_name = None
fields = ()
param_defaults = {}
id = None
def __init__(self, **kwargs):
if self.verbose_name is None:
self.verbose_name = self.api_name
for (param, default) in self.param_defaults.items():
setattr(self, param, kwargs.get(param, default))
@classmethod
def new_from_json(cls, data, **kwargs):
""" Create a new instance based on a JSON dict. Any kwargs should be
supplied by the inherited, calling class.
Args:
data: A JSON dict, as converted from the JSON in the orchestra API.
"""
if data is None:
return cls()
json_data = data.copy()
if kwargs:
for key, val in kwargs.items():
json_data[key] = val
c = cls(**json_data)
c._json = data
return c
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self)
def __str__(self):
return '%s object (%s)' % (self.__class__.__name__, self.id)
class Bill(OrchestraModel):
api_name = 'bill'
param_defaults = {
"id": None,
"number": "1",
"type": "INVOICE",
"total": 0.0,
"is_sent": False,
"created_on": "",
"due_on": "",
"comments": "",
}
class BillingContact(OrchestraModel):
param_defaults = {
'name': None,
'address': None,
'city': None,
'zipcode': None,
'country': None,
'vat': None,
}
class PaymentSource(OrchestraModel):
api_name = 'payment-source'
param_defaults = {
"method": None,
"data": [],
"is_active": False,
}
def __init__(self, **kwargs):
super().__init__(**kwargs)
# payment details are passed as a plain string
# try to convert to a python structure
try:
self.data = ast.literal_eval(self.data)
except (ValueError, SyntaxError) as e:
logger.error(e)
class UserAccount(OrchestraModel):
api_name = 'accounts'
param_defaults = {
'username': None,
'type': None,
'language': None,
'short_name': None,
'full_name': None,
'billing': {},
'last_login': None,
}
@classmethod
def new_from_json(cls, data, **kwargs):
billing = None
language = None
last_login = None
if 'billcontact' in data:
billing = BillingContact.new_from_json(data['billcontact'])
# Django expects that language code is lowercase
if 'language' in data:
language = data['language'].lower()
last_login = data.get('last_login')
if last_login is not None:
last_login = parse_datetime(last_login)
return super().new_from_json(data=data, billing=billing, language=language, last_login=last_login)
def allowed_resources(self, resource):
allowed_by_type = musician_settings.ALLOWED_RESOURCES[self.type]
return allowed_by_type[resource]
class DatabaseUser(OrchestraModel):
api_name = 'databaseusers'
fields = ('username',)
param_defaults = {
'username': None,
}
class DatabaseService(OrchestraModel):
api_name = 'database'
verbose_name = _('Databases')
description = _('Description details for databases page.')
fields = ('name', 'type', 'users')
param_defaults = {
"id": None,
"name": None,
"type": None,
"users": None,
"usage": {},
}
@classmethod
def new_from_json(cls, data, **kwargs):
users = None
if 'users' in data:
users = [DatabaseUser.new_from_json(user_data) for user_data in data['users']]
usage = cls.get_usage(data)
return super().new_from_json(data=data, users=users, usage=usage)
@classmethod
def get_usage(cls, data):
try:
resources = data['resources']
resource_disk = {}
for r in resources:
if r['name'] == 'disk':
resource_disk = r
break
details = {
'used': float(resource_disk['used']),
'total': resource_disk['allocated'],
'unit': resource_disk['unit'],
}
except (IndexError, KeyError):
return {}
percent = get_bootstraped_percent(
details['used'],
details['total']
)
details['percent'] = percent
return details
@property
def manager_url(self):
return musician_settings.URL_DB_PHPMYADMIN
class Domain(OrchestraModel):
api_name = 'domain'
param_defaults = {
"id": None,
"name": None,
"records": [],
"addresses": [],
"usage": {},
"websites": [],
"url": None,
}
@classmethod
def new_from_json(cls, data, **kwargs):
records = cls.param_defaults.get("records")
if 'records' in data:
records = [DomainRecord.new_from_json(record_data) for record_data in data['records']]
return super().new_from_json(data=data, records=records)
def __str__(self):
return self.name
class DomainRecord(OrchestraModel):
param_defaults = {
"type": None,
"value": None,
}
def __str__(self):
return '<%s: %s>' % (self.type, self.value)
class Address(OrchestraModel):
api_name = 'address'
verbose_name = _('Mail addresses')
description = _('Description details for mail addresses page.')
fields = ('mail_address', 'aliases', 'type', 'type_detail')
param_defaults = {
"id": None,
"name": None,
"domain": None,
"mailboxes": [],
"forward": None,
'url': None,
}
FORWARD = 'forward'
MAILBOX = 'mailbox'
def __init__(self, **kwargs):
self.data = kwargs
super().__init__(**kwargs)
def deserialize(self):
data = {
'name': self.data['name'],
'domain': self.data['domain']['url'],
'mailboxes': [mbox['url'] for mbox in self.data['mailboxes']],
'forward': self.data['forward'],
}
return data
@property
def aliases(self):
return [
name + '@' + self.data['domain']['name'] for name in self.data['names'][1:]
]
@property
def full_address_name(self):
return "{}@{}".format(self.name, self.domain['name'])
@property
def type(self):
if self.data['forward']:
return self.FORWARD
return self.MAILBOX
@property
def type_detail(self):
if self.type == self.FORWARD:
return self.data['forward']
# retrieve mailbox usage
try:
resource = self.data['mailboxes'][0]['resources']
resource_disk = {}
for r in resource:
if r['name'] == 'disk':
resource_disk = r
break
mailbox_details = {
'used': float(resource_disk['used']),
'total': resource_disk['allocated'],
'unit': resource_disk['unit'],
}
percent = get_bootstraped_percent(
mailbox_details['used'],
mailbox_details['total']
)
mailbox_details['percent'] = percent
except (IndexError, KeyError):
mailbox_details = {}
return mailbox_details
class Mailbox(OrchestraModel):
api_name = 'mailbox'
verbose_name = _('Mailbox')
description = _('Description details for mailbox page.')
fields = ('name', 'filtering', 'addresses', 'active')
param_defaults = {
'id': None,
'name': None,
'filtering': None,
'is_active': True,
'addresses': [],
'url': None,
}
@classmethod
def new_from_json(cls, data, **kwargs):
addresses = [Address.new_from_json(addr) for addr in data.get('addresses', [])]
return super().new_from_json(data=data, addresses=addresses)
def deserialize(self):
data = {
'addresses': [addr.url for addr in self.addresses],
}
return data
class MailinglistService(OrchestraModel):
api_name = 'mailinglist'
verbose_name = _('Mailing list')
description = _('Description details for mailinglist page.')
fields = ('name', 'status', 'address_name', 'admin_email', 'configure')
param_defaults = {
'name': None,
'is_active': True,
'admin_email': None,
}
def __init__(self, **kwargs):
self.data = kwargs
super().__init__(**kwargs)
@property
def address_name(self):
address_domain = self.data['address_domain']
if address_domain is None:
return self.data['address_name']
return "{}@{}".format(self.data['address_name'], address_domain['name'])
@property
def manager_url(self):
return musician_settings.URL_MAILTRAIN
class SaasService(OrchestraModel):
api_name = 'saas'
verbose_name = _('Software as a Service (SaaS)')
description = _('Description details for SaaS page.')
param_defaults = {
'name': None,
'service': None,
'is_active': True,
'data': {},
}
@property
def manager_url(self):
URLS = {
'gitlab': musician_settings.URL_SAAS_GITLAB,
'owncloud': musician_settings.URL_SAAS_OWNCLOUD,
'wordpress': musician_settings.URL_SAAS_WORDPRESS,
}
return URLS.get(self.service, '#none')
class WebSite(OrchestraModel):
api_name = 'website'
param_defaults = {
"id": None,
"name": None,
"protocol": None,
"is_active": True,
"domains": [],
"contents": [],
}
@classmethod
def new_from_json(cls, data, **kwargs):
domains = cls.param_defaults.get("domains")
if 'domains' in data:
domains = [Domain.new_from_json(domain_data) for domain_data in data['domains']]
return super().new_from_json(data=data, domains=domains)

Some files were not shown because too many files have changed in this diff Show More