initial commit
This commit is contained in:
commit
ce809235e5
34 changed files with 736 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
db.sqlite3
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
https://service.snom.com/display/wiki/DHCP+options#DHCPoptions-Auto-provisioningoptions
|
||||||
|
|
23
manage.py
Executable file
23
manage.py
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snom.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
0
phonebook/__init__.py
Normal file
0
phonebook/__init__.py
Normal file
27
phonebook/admin.py
Normal file
27
phonebook/admin.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import PhonebookEntry, PhonebookNumber
|
||||||
|
|
||||||
|
|
||||||
|
class PhonebookNumberInline(admin.TabularInline):
|
||||||
|
model = PhonebookNumber
|
||||||
|
|
||||||
|
|
||||||
|
@admin.display(description="Name")
|
||||||
|
def first_and_last_name(obj):
|
||||||
|
return f"{obj.first_name} {obj.last_name}"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PhonebookEntry)
|
||||||
|
class PhonebookEntryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (first_and_last_name, "enabled", "favourite", "vip", "blocked")
|
||||||
|
list_filter = ("first_name", "last_name", "enabled", "favourite", "vip", "blocked")
|
||||||
|
|
||||||
|
inlines = [
|
||||||
|
PhonebookNumberInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PhonebookNumber)
|
||||||
|
class PhonebookNumberAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
6
phonebook/apps.py
Normal file
6
phonebook/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PhonebookConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "phonebook"
|
71
phonebook/migrations/0001_initial.py
Normal file
71
phonebook/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 06:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PhonebookEntry",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("first_name", models.CharField(max_length=255)),
|
||||||
|
("last_name", models.CharField(max_length=255)),
|
||||||
|
("enabled", models.BooleanField(default=True)),
|
||||||
|
("blocked", models.BooleanField(default=False)),
|
||||||
|
("favourite", models.BooleanField(default=False)),
|
||||||
|
("vip", models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PhonebookNumber",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("number", models.IntegerField()),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("SIP", "SIP"),
|
||||||
|
("MOBILE", "Mobile"),
|
||||||
|
("FIXED", "Fixed"),
|
||||||
|
("HOME", "Home"),
|
||||||
|
("BUSINESS", "Business"),
|
||||||
|
("EXTENSION", "Extension"),
|
||||||
|
],
|
||||||
|
default="SIP",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="numbers",
|
||||||
|
to="phonebook.phonebookentry",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 06:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("phonebook", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="phonebookentry",
|
||||||
|
options={
|
||||||
|
"ordering": ["first_name", "last_name"],
|
||||||
|
"verbose_name_plural": "Phonebook entries",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="phonebooknumber",
|
||||||
|
options={
|
||||||
|
"ordering": ["number"],
|
||||||
|
"verbose_name_plural": "Phonebook numbers",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="phonebooknumber",
|
||||||
|
name="number",
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 08:00
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('phonebook', '0002_alter_phonebookentry_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='phonebookentry',
|
||||||
|
options={'ordering': ['first_name', 'last_name'], 'verbose_name_plural': 'Phonebook Entries'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='phonebooknumber',
|
||||||
|
options={'ordering': ['number'], 'verbose_name_plural': 'Phonebook Numbers'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='phonebooknumber',
|
||||||
|
name='number',
|
||||||
|
field=models.CharField(max_length=255, validators=[django.core.validators.RegexValidator(message='Phone number must be entered in the format: +999999999', regex='^\\+[0-9]+$')]),
|
||||||
|
),
|
||||||
|
]
|
0
phonebook/migrations/__init__.py
Normal file
0
phonebook/migrations/__init__.py
Normal file
55
phonebook/models.py
Normal file
55
phonebook/models.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
class PhonebookEntry(models.Model):
|
||||||
|
first_name = models.CharField(max_length=255)
|
||||||
|
last_name = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
enabled = models.BooleanField(default=True)
|
||||||
|
blocked = models.BooleanField(default=False)
|
||||||
|
favourite = models.BooleanField(default=False)
|
||||||
|
vip = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = _("Phonebook Entries")
|
||||||
|
ordering = ["first_name", "last_name"]
|
||||||
|
|
||||||
|
|
||||||
|
class PhonebookNumber(models.Model):
|
||||||
|
class ContactTypes(models.TextChoices):
|
||||||
|
SIP = "SIP", _("SIP")
|
||||||
|
MOBILE = "MOBILE", _("Mobile")
|
||||||
|
FIXED = "FIXED", _("Fixed")
|
||||||
|
HOME = "HOME", _("Home")
|
||||||
|
BUSINESS = "BUSINESS", _("Business")
|
||||||
|
EXTENSION = "EXTENSION", _("Extension")
|
||||||
|
|
||||||
|
number = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
regex="^\+[0-9]+$",
|
||||||
|
message="Phone number must be entered in the format: +999999999",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=20, choices=ContactTypes, default=ContactTypes.SIP
|
||||||
|
)
|
||||||
|
|
||||||
|
contact = models.ForeignKey(
|
||||||
|
PhonebookEntry, on_delete=models.CASCADE, related_name="numbers"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.number} ({self.contact.first_name} {self.contact.last_name}, {self.type.title()})"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = _("Phonebook Numbers")
|
||||||
|
ordering = ["number"]
|
15
phonebook/templates/phonebook/index.xml
Normal file
15
phonebook/templates/phonebook/index.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<tbook e='2' version='2.0'>
|
||||||
|
{% for entry in entries %}
|
||||||
|
<contact fav="{{ entry.favourite | lower }}"
|
||||||
|
vip="{{ entry.vip | lower }}"
|
||||||
|
blocked="{{ entry.blocked | lower }}">
|
||||||
|
<first_name>{{ entry.first_name }}</first_name>
|
||||||
|
<last_name>{{ entry.last_name }}</last_name>
|
||||||
|
<numbers>
|
||||||
|
{% for number in entry.numbers.all %}
|
||||||
|
<number no="{{ number.number }}" type="{{ number.type | lower }}" outgoing_id="0"/>
|
||||||
|
{% endfor %}
|
||||||
|
</numbers>
|
||||||
|
</contact>
|
||||||
|
{% endfor %}
|
||||||
|
</tbook>
|
3
phonebook/tests.py
Normal file
3
phonebook/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
7
phonebook/urls.py
Normal file
7
phonebook/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.index, name="phonebook_index"),
|
||||||
|
]
|
8
phonebook/views.py
Normal file
8
phonebook/views.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from phonebook.models import PhonebookEntry
|
||||||
|
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
entries = PhonebookEntry.objects.all().prefetch_related("numbers")
|
||||||
|
context = {"entries": entries}
|
||||||
|
return render(request, "phonebook/index.xml", context, content_type="text/xml")
|
0
settings/__init__.py
Normal file
0
settings/__init__.py
Normal file
30
settings/admin.py
Normal file
30
settings/admin.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import SIPAccount, SnomPhone, SnomPhoneType, SnomFunctionKey
|
||||||
|
|
||||||
|
|
||||||
|
class SnomFunctionKeyInline(admin.TabularInline):
|
||||||
|
model = SnomFunctionKey
|
||||||
|
|
||||||
|
|
||||||
|
@admin.display(description="SIP Account")
|
||||||
|
def sip_username_ip(obj):
|
||||||
|
return f"{obj.username}@{obj.ip}"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SIPAccount)
|
||||||
|
class SIPAccountAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (sip_username_ip, 'display_name', 'tone_scheme')
|
||||||
|
list_filter = ('ip', 'tone_scheme')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SnomPhone)
|
||||||
|
class SnomPhoneAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('phone_name', 'mac_address', 'sip_account')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SnomPhoneType)
|
||||||
|
class SnomPhoneTypeAdmin(admin.ModelAdmin):
|
||||||
|
inlines = [
|
||||||
|
SnomFunctionKeyInline
|
||||||
|
]
|
6
settings/apps.py
Normal file
6
settings/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'settings'
|
37
settings/migrations/0001_initial.py
Normal file
37
settings/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 08:00
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SIPAccount',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ip', models.GenericIPAddressField()),
|
||||||
|
('username', models.CharField(max_length=255)),
|
||||||
|
('password', models.CharField(max_length=255)),
|
||||||
|
('display_name', models.CharField(max_length=255)),
|
||||||
|
('tone_scheme', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SnomPhone',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('mac_address', models.CharField(max_length=12, unique=True, validators=[django.core.validators.RegexValidator('^[0-9A-F]{12}$')])),
|
||||||
|
('phone_name', models.CharField(max_length=255)),
|
||||||
|
('admin_password', models.CharField(max_length=255)),
|
||||||
|
('sip_account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='settings.sipaccount')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 08:16
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('settings', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='sipaccount',
|
||||||
|
options={'ordering': ['display_name'], 'verbose_name': 'SIP Account', 'verbose_name_plural': 'SIP Accounts'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='snomphone',
|
||||||
|
options={'ordering': ['phone_name'], 'verbose_name': 'Snom Phone', 'verbose_name_plural': 'Snom Phones'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='snomphone',
|
||||||
|
name='id',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='snomphone',
|
||||||
|
name='timezone',
|
||||||
|
field=models.CharField(default='GBR-0', help_text='https://service.snom.com/display/wiki/timezone', max_length=10),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='snomphone',
|
||||||
|
name='mac_address',
|
||||||
|
field=models.CharField(max_length=12, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator('^[0-9A-F]{12}$')]),
|
||||||
|
),
|
||||||
|
]
|
35
settings/migrations/0003_snomphonetype_snomfunctionkey.py
Normal file
35
settings/migrations/0003_snomphonetype_snomfunctionkey.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 08:43
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('settings', '0002_alter_sipaccount_options_alter_snomphone_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SnomPhoneType',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('model', models.CharField(max_length=255)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SnomFunctionKey',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('key_id', models.IntegerField()),
|
||||||
|
('label', models.CharField(max_length=255)),
|
||||||
|
('value', models.CharField(max_length=255)),
|
||||||
|
('phone_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='settings.snomphonetype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['phone_type', 'key_id'],
|
||||||
|
'constraints': [models.UniqueConstraint(fields=('phone_type', 'key_id'), name='phone_type_key_id_unique')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
20
settings/migrations/0004_snomphone_phone_type.py
Normal file
20
settings/migrations/0004_snomphone_phone_type.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 08:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('settings', '0003_snomphonetype_snomfunctionkey'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='snomphone',
|
||||||
|
name='phone_type',
|
||||||
|
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.PROTECT, to='settings.snomphonetype'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-15 09:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('settings', '0004_snomphone_phone_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='snomphonetype',
|
||||||
|
name='phonebook_function_key',
|
||||||
|
field=models.IntegerField(default=-1, help_text='if set to anything >+0, phonebook will be provisioned on this key'),
|
||||||
|
),
|
||||||
|
]
|
0
settings/migrations/__init__.py
Normal file
0
settings/migrations/__init__.py
Normal file
64
settings/models.py
Normal file
64
settings/models.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
|
||||||
|
|
||||||
|
class SIPAccount(models.Model):
|
||||||
|
ip = models.GenericIPAddressField()
|
||||||
|
username = models.CharField(max_length=255)
|
||||||
|
password = models.CharField(max_length=255)
|
||||||
|
display_name = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
tone_scheme = models.CharField(max_length=10)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.username}@{self.ip}'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "SIP Account"
|
||||||
|
verbose_name_plural = "SIP Accounts"
|
||||||
|
ordering = ['display_name']
|
||||||
|
|
||||||
|
|
||||||
|
class SnomPhoneType(models.Model):
|
||||||
|
model = models.CharField(max_length=255)
|
||||||
|
phonebook_function_key = models.IntegerField(default=-1, help_text='if set to anything >+0, phonebook will be provisioned on this key')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.model
|
||||||
|
|
||||||
|
|
||||||
|
class SnomFunctionKey(models.Model):
|
||||||
|
phone_type = models.ForeignKey(SnomPhoneType, on_delete=models.CASCADE)
|
||||||
|
key_id = models.IntegerField()
|
||||||
|
|
||||||
|
label = models.CharField(max_length=255)
|
||||||
|
value = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.phone_type.model}:{self.key_id}'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['phone_type', 'key_id']
|
||||||
|
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['phone_type', 'key_id'], name='phone_type_key_id_unique')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SnomPhone(models.Model):
|
||||||
|
mac_address = models.CharField(max_length=12, validators=[RegexValidator('^[0-9A-F]{12}$')], primary_key=True)
|
||||||
|
phone_name = models.CharField(max_length=255)
|
||||||
|
admin_password = models.CharField(max_length=255)
|
||||||
|
timezone = models.CharField(max_length=10, help_text="https://service.snom.com/display/wiki/timezone", default="GBR-0")
|
||||||
|
|
||||||
|
sip_account = models.ForeignKey(SIPAccount, on_delete=models.PROTECT)
|
||||||
|
phone_type = models.ForeignKey(SnomPhoneType, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.phone_name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Snom Phone"
|
||||||
|
verbose_name_plural = "Snom Phones"
|
||||||
|
ordering = ['phone_name']
|
||||||
|
|
35
settings/templates/settings/settings.xml
Normal file
35
settings/templates/settings/settings.xml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<settings>
|
||||||
|
<phone-settings>
|
||||||
|
<update_policy perm="R">auto_update</update_policy>
|
||||||
|
<phone_name perm="R">{{ settings.phone_name }}</phone_name>
|
||||||
|
<language perm="">English</language>
|
||||||
|
<web_language perm="">English</web_language>
|
||||||
|
<date_us_format perm="R">off</date_us_format>
|
||||||
|
<dialnumber_us_format perm="R">off</dialnumber_us_format>
|
||||||
|
<timezone>{{ settings.timezone }}</timezone>
|
||||||
|
<time_server perm="R">pool.ntp.org</time_server>
|
||||||
|
<tone_scheme perm="R">{{ settings.sip_account.tone_scheme }}</tone_scheme>
|
||||||
|
|
||||||
|
<user_name idx="1" perm="R">{{ settings.sip_account.username }}</user_name>
|
||||||
|
<user_pass idx="1" perm="R">{{ settings.sip_account.password }}</user_pass>
|
||||||
|
<user_host idx="1" perm="R">{{ settings.sip_account.ip }}</user_host>
|
||||||
|
<user_realname idx="1" perm="R">{{ settings.sip_account.display_name }}</user_realname>
|
||||||
|
<user_active idx="1" perm="R">on</user_active>
|
||||||
|
|
||||||
|
<functionKeys e="2">
|
||||||
|
{% for key in function_keys.all %}
|
||||||
|
{% if settings.phone_type.phonebook_function_key != key.key_id %}
|
||||||
|
<fkey idx="{{ key.key_id }}" context="active" label="{{ key.label}}" lp="on" default_text="$name" perm="R">{{ key.value }}</fkey>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if settings.phone_type.phonebook_function_key >= 0 %}
|
||||||
|
<fkey idx="{{ settings.phone_type.phonebook_function_key }}" context="active" label="Phonebook" lp="on" default_text="$name" perm="R">url {{ phonebook_url }}</fkey>
|
||||||
|
{% endif %}
|
||||||
|
</functionKeys>
|
||||||
|
|
||||||
|
<admin_mode_password perm="R">{{ settings.admin_password }}</admin_mode_password>
|
||||||
|
<http_user perm="R">snom</http_user>
|
||||||
|
<http_pass perm="R">{{ settings.admin_password }}</http_pass>
|
||||||
|
</phone-settings>
|
||||||
|
</settings>
|
3
settings/tests.py
Normal file
3
settings/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
7
settings/urls.py
Normal file
7
settings/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("settings-<str:mac_address>.xml", views.mac_settings, name="mac_settings"),
|
||||||
|
]
|
17
settings/views.py
Normal file
17
settings/views.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from settings.models import SnomPhone, SnomFunctionKey
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
def mac_settings(request, mac_address):
|
||||||
|
settings = get_object_or_404(SnomPhone, mac_address=mac_address)
|
||||||
|
function_keys = SnomFunctionKey.objects.filter(phone_type=settings.phone_type)
|
||||||
|
|
||||||
|
phonebook_url = request.build_absolute_uri(reverse("phonebook_index"))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"settings": settings,
|
||||||
|
"function_keys": function_keys,
|
||||||
|
"phonebook_url": phonebook_url,
|
||||||
|
}
|
||||||
|
return render(request, "settings/settings.xml", context, content_type="text/xml")
|
0
snom/__init__.py
Normal file
0
snom/__init__.py
Normal file
16
snom/asgi.py
Normal file
16
snom/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for snom project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snom.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
112
snom/settings.py
Normal file
112
snom/settings.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = "django-insecure-rhrs+9%s+#cc)xkqto*(%sv+d2*!@_q=(u2ss+y)w9g5-yp(fc"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"phonebook.apps.PhonebookConfig",
|
||||||
|
"settings.apps.SettingsConfig",
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "snom.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "snom.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
8
snom/urls.py
Normal file
8
snom/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("phonebook/", include("phonebook.urls")),
|
||||||
|
path("snom/", include("settings.urls")),
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
]
|
16
snom/wsgi.py
Normal file
16
snom/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for snom project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "snom.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
Loading…
Add table
Add a link
Reference in a new issue