initial commit

This commit is contained in:
Franzi 2025-06-15 11:27:37 +02:00
commit ce809235e5
Signed by: kunsi
GPG key ID: 12E3D2136B818350
34 changed files with 736 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
db.sqlite3

2
README.md Normal file
View file

@ -0,0 +1,2 @@
https://service.snom.com/display/wiki/DHCP+options#DHCPoptions-Auto-provisioningoptions

23
manage.py Executable file
View 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
View file

27
phonebook/admin.py Normal file
View 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
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PhonebookConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "phonebook"

View 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",
),
),
],
),
]

View file

@ -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),
),
]

View file

@ -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]+$')]),
),
]

View file

55
phonebook/models.py Normal file
View 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"]

View 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
View file

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

7
phonebook/urls.py Normal file
View 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
View 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
View file

30
settings/admin.py Normal file
View 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
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SettingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'settings'

View 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')),
],
),
]

View file

@ -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}$')]),
),
]

View 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')],
},
),
]

View 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,
),
]

View file

@ -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'),
),
]

View file

64
settings/models.py Normal file
View 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']

View 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
View file

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

7
settings/urls.py Normal file
View 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
View 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
View file

16
snom/asgi.py Normal file
View 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
View 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
View 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
View 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()