Jelajahi Sumber

update directories

Староверов Данила Андреевич 2 tahun lalu
induk
melakukan
c3122ded42
69 mengubah file dengan 0 tambahan dan 4691 penghapusan
  1. 0 4
      metaservicesynced/.gitignore
  2. 0 0
      metaservicesynced/__init__.py
  3. 0 222
      metaservicesynced/admin.py
  4. 0 17
      metaservicesynced/apiview.py
  5. 0 6
      metaservicesynced/apps.py
  6. 0 874
      metaservicesynced/migrations/0001_initial.py
  7. 0 0
      metaservicesynced/migrations/__init__.py
  8. 0 235
      metaservicesynced/models.py
  9. 0 8
      metaservicesynced/serializer.py
  10. 0 3
      metaservicesynced/tests.py
  11. 0 12
      metaservicesynced/urls.py
  12. 0 5
      metaservicesynced/views.py
  13. 0 5
      tickets/.gitignore
  14. 0 77
      tickets/README.md
  15. 0 0
      tickets/__init__.py
  16. 0 48
      tickets/admin.py
  17. 0 77
      tickets/admin_utils.py
  18. 0 30
      tickets/apiviews.py
  19. 0 7
      tickets/apps.py
  20. 0 22
      tickets/defaults.py
  21. 0 64
      tickets/forms.py
  22. 0 0
      tickets/mail/__init__.py
  23. 0 9
      tickets/mail/consumers/__init__.py
  24. 0 171
      tickets/mail/consumers/tracker.py
  25. 0 27
      tickets/mail/delivery.py
  26. 0 9
      tickets/mail/producers/__init__.py
  27. 0 98
      tickets/mail/producers/imap.py
  28. 0 0
      tickets/management/__init__.py
  29. 0 0
      tickets/management/commands/__init__.py
  30. 0 41
      tickets/management/commands/mail_worker.py
  31. 0 261
      tickets/migrations/0001_initial.py
  32. 0 0
      tickets/migrations/__init__.py
  33. 0 221
      tickets/models.py
  34. 0 0
      tickets/operations/__init__.py
  35. 0 201
      tickets/operations/csv_importer.py
  36. 0 32
      tickets/queries.py
  37. 0 19
      tickets/requirements.txt
  38. 0 27
      tickets/serializer.py
  39. 0 382
      tickets/static/tickets/js/jquery.tablednd_0_5.js
  40. 0 5
      tickets/template_tags/custom_tags.py
  41. 0 83
      tickets/templates/tickets/base.html
  42. 0 19
      tickets/templates/tickets/create_list.html
  43. 0 20
      tickets/templates/tickets/del_list.html
  44. 0 17
      tickets/templates/tickets/email/assigned_body.txt
  45. 0 1
      tickets/templates/tickets/email/assigned_subject.txt
  46. 0 17
      tickets/templates/tickets/email/changetask_body.txt
  47. 0 16
      tickets/templates/tickets/email/newcomment_body.txt
  48. 0 84
      tickets/templates/tickets/import_csv.html
  49. 0 1
      tickets/templates/tickets/include/delete_list.html
  50. 0 41
      tickets/templates/tickets/include/ticket_create.html
  51. 0 42
      tickets/templates/tickets/include/ticket_edit.html
  52. 0 109
      tickets/templates/tickets/list_detail.html
  53. 0 30
      tickets/templates/tickets/list_lists.html
  54. 0 29
      tickets/templates/tickets/search_results.html
  55. 0 188
      tickets/templates/tickets/task_detail.html
  56. 0 36
      tickets/urls.py
  57. 0 198
      tickets/utils.py
  58. 0 11
      tickets/views/__init__.py
  59. 0 42
      tickets/views/change_status.py
  60. 0 47
      tickets/views/create_list.py
  61. 0 34
      tickets/views/del_list.py
  62. 0 42
      tickets/views/delete_task.py
  63. 0 34
      tickets/views/import_csv.py
  64. 0 57
      tickets/views/list_detail.py
  65. 0 42
      tickets/views/list_lists.py
  66. 0 40
      tickets/views/remove_attachment.py
  67. 0 35
      tickets/views/reorder_tasks.py
  68. 0 43
      tickets/views/search.py
  69. 0 114
      tickets/views/task_detail.py

+ 0 - 4
metaservicesynced/.gitignore

@@ -1,4 +0,0 @@
-# Python
-__pycache__/
-*.py[cod]
-*$py.class

+ 0 - 0
metaservicesynced/__init__.py


+ 0 - 222
metaservicesynced/admin.py

@@ -1,222 +0,0 @@
-from django.contrib import admin
-from .models import *
-
-
-@admin.register(Company)
-class CompanyAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'legal_name',
-        'repr_id',
-        'inn',
-        'kpp',
-        'ogrn',
-        'bank_name',
-        'bik',
-        'ks',
-        'rs',
-        'address',
-        'requirements',
-        'status',
-        'ticket_status',
-        'id_metaservice',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = ('repr_id', 'ticket_status')
-
-
-@admin.register(Permissions)
-class PermissionsAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'check_date',
-        'id_permissions',
-        'check_level',
-        'checked_by',
-        'user_id',
-        'status',
-        'ticket_status',
-        'id_metaservice',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = ('check_date', 'checked_by', 'user_id', 'ticket_status')
-
-
-@admin.register(ServiceType)
-class ServiceTypeAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'codename',
-        'caption',
-        'description',
-        'requirements',
-        'price_type',
-        'status',
-        'ticket_status',
-        'id_metaservice',
-        'link_agreement',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = ('ticket_status',)
-
-
-@admin.register(Provider)
-class ProviderAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'type',
-        'company_id',
-        'user_id',
-        'id_metaservice',
-        'requirements',
-        'status',
-        'ticket_status',
-        'location_type',
-        'default_location',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = ('company_id', 'user_id', 'ticket_status')
-
-
-@admin.register(Resource)
-class ResourceAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'type_id',
-        'user_id',
-        'requirements',
-        'status',
-        'ticket_status',
-        'id_metaservice',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = ('user_id', 'ticket_status')
-
-
-@admin.register(Service)
-class ServiceAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'servicetype_id',
-        'id_provider',
-        'resource_id',
-        'requirements',
-        'id_metaservice',
-        'price_alg',
-        'price_km',
-        'price_min',
-        'price_amount',
-        'service_status',
-        'status',
-        'ticket_status',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = (
-        'servicetype_id',
-        'id_provider',
-        'resource_id',
-        'ticket_status',
-    )
-
-
-@admin.register(Documents)
-class DocumentsAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'check_date',
-        'check_level',
-        'expire_date',
-        'id_metaservice',
-        'requirements',
-        'status',
-        'ticket_status',
-        'datalink',
-        'doc_type',
-        'user_id',
-        'company_id',
-        'is_global',
-        'is_visible',
-        'checked_by',
-    )
-    list_filter = (
-        'check_date',
-        'expire_date',
-        'ticket_status',
-        'user_id',
-        'company_id',
-        'checked_by',
-    )
-
-
-@admin.register(Client)
-class ClientAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'user',
-        'requirements',
-        'status',
-        'ticket_status',
-        'id_metaservice',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = ('user', 'ticket_status')
-
-
-@admin.register(Orders)
-class OrdersAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'service',
-        'service_type',
-        'state',
-        'id_metaservice',
-        'provider',
-        'receiver',
-        'client_id',
-        'time_created',
-        'time_placed',
-        'time_start',
-        'time_finish_predicted',
-        'time_finish_real',
-        'ticket',
-        'predicted_price',
-        'real_price',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = (
-        'service',
-        'service_type',
-        'provider',
-        'receiver',
-        'client_id',
-        'time_created',
-        'time_placed',
-        'time_start',
-        'time_finish_predicted',
-        'time_finish_real',
-    )
-
-
-@admin.register(Relationship)
-class RelationshipAdmin(admin.ModelAdmin):
-    list_display = (
-        'id',
-        'user_id_who',
-        'user_id_whom',
-        'neg_type',
-        'id_metaservice',
-        'requirements',
-        'status',
-        'ticket_status',
-        'is_global',
-        'is_visible',
-    )
-    list_filter = ('user_id_who', 'user_id_whom', 'ticket_status')

+ 0 - 17
metaservicesynced/apiview.py

@@ -1,17 +0,0 @@
-from .serializer import *
-from rest_framework import viewsets, permissions, exceptions
-from rest_framework.authentication import TokenAuthentication
-from rest_framework.decorators import action
-from metaservicesynced.models import *
-from rest_framework.views import APIView
-from rest_framework.response import Response
-
-class DocumentsMVS(viewsets.ModelViewSet):
-    """
-    
-    """
-
-    queryset = Documents.objects.all()
-    serializer_class = DocumentsSerializer
-    #permission_classes = [IsOwnerOrReadOnly]
-    permission_classes = [permissions.IsAuthenticated]

+ 0 - 6
metaservicesynced/apps.py

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

+ 0 - 874
metaservicesynced/migrations/0001_initial.py

@@ -1,874 +0,0 @@
-# Generated by Django 4.1.3 on 2023-03-31 20:21
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-        ("tickets", "0001_initial"),
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name="Client",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                ("requirements", models.CharField(max_length=150)),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="активность на основе системы заявок", max_length=150
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.",
-                        null=True,
-                    ),
-                ),
-                ("is_global", models.CharField(max_length=1)),
-                ("is_visible", models.CharField(max_length=1)),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-                (
-                    "user",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "client",
-            },
-        ),
-        migrations.CreateModel(
-            name="Company",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "legal_name",
-                    models.CharField(
-                        help_text="настоящее имя юридического лица", max_length=150
-                    ),
-                ),
-                (
-                    "inn",
-                    models.CharField(
-                        help_text="ИНН компании", max_length=10, unique=True
-                    ),
-                ),
-                ("kpp", models.CharField(help_text="КПП компании", max_length=9)),
-                ("ogrn", models.CharField(help_text="ОГРН компании", max_length=13)),
-                (
-                    "bank_name",
-                    models.CharField(
-                        help_text="Название банка с расчетным счетом", max_length=150
-                    ),
-                ),
-                ("bik", models.CharField(help_text="БИК компании", max_length=9)),
-                (
-                    "ks",
-                    models.CharField(
-                        help_text="Корреспондентский счёт (счёт, открываемый банковской организацией в подразделении самого банка)",
-                        max_length=50,
-                    ),
-                ),
-                ("rs", models.CharField(help_text="Расчетный счет", max_length=50)),
-                (
-                    "address",
-                    models.CharField(help_text="Юридический адрес", max_length=150),
-                ),
-                (
-                    "requirements",
-                    models.CharField(
-                        help_text="код необходимого для того, чтобы ресурс мог стать активным",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="статус обработки заявки в системе заявок",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.",
-                        null=True,
-                    ),
-                ),
-                (
-                    "is_global",
-                    models.CharField(
-                        help_text="доступны ли документы для хранения в глобальном сервисе/нужна синхронизация",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "is_visible",
-                    models.CharField(
-                        help_text="доступна ли информация о наличии документов для планирования в цепочке с другими услугами в глобальном сервисе",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "repr_id",
-                    models.ForeignKey(
-                        help_text="уникальный идентификатор представителя компании. Это обязательно пользователь-провайдер определенного типа. То есть нельзя назначить ответственного, который не может быть ответственным.",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "company",
-            },
-        ),
-        migrations.CreateModel(
-            name="Provider",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "type",
-                    models.CharField(
-                        help_text="тип поставщика (партнер/ответственное лицо/поставщик услуг). Смысл такой - провайдер это статус пользователя, который, в зависимости от применения, может нести разный смысл и подразумевает под собой какой-то тип действия. Обычные исполнители - это провайдеры услуг (код 3). Ответственные за какое-то имущество, которые сдают его в аренду - это тоже провайдеры (код 2). Ответственные за набор услуг перед метасервисом (фактически - назначенные админы) - это провайдеры-партнеры (код 1)",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер.",
-                        null=True,
-                    ),
-                ),
-                (
-                    "requirements",
-                    models.CharField(
-                        help_text="требования для того, чтобы можно было предоставлять услуги любые в этом метасервисе в целом (самые строгие)",
-                        max_length=300,
-                    ),
-                ),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="статус пользователя в системе относительно прохождения проверок (activity_status) (может быть active только в том случае, если ticket, влияющий на статус - закрыт.",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "location_type",
-                    models.CharField(
-                        help_text="статическая или динамическая локация оказания услуги. Если статическая, а исполнитель находится существенно за пределами локации - то тогда статус автоматом оффлайн для приема новых заявок.",
-                        max_length=300,
-                    ),
-                ),
-                (
-                    "default_location",
-                    models.CharField(
-                        help_text="локация по умолчанию для объекта.", max_length=300
-                    ),
-                ),
-                (
-                    "is_global",
-                    models.CharField(
-                        help_text="(аккаунт поставщика услуг) – доступен для планирования в цепочке с другими услугами в глобальном сервисе",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "is_visible",
-                    models.CharField(
-                        help_text="(аккаунт поставщика услуг) – доступен для хранения в  глобальном сервисе/необходима синхронизация",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "company_id",
-                    models.ForeignKey(
-                        help_text="уникальный идентификатор компании, от лица которой выступает провайдер. Смысл такой - ответственны могут быть только одушевленные лица, компании - не одушевленные. Все услуги предоставляются через компании-партнеры, самозанятые или ИП являются единицами таких компаний.",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.company",
-                    ),
-                ),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-                (
-                    "user_id",
-                    models.ForeignKey(
-                        help_text="уникальный идентификатор конкретного пользователя системы (meta-user), который будет оказывать услугу. Один пользователь может быть провайдером нескольких услуг. Статус провайдера означает, что с данным пользователем может быть установлена связь, как с исполнителем.",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "provider",
-            },
-        ),
-        migrations.CreateModel(
-            name="Resource",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "type_id",
-                    models.CharField(help_text="идентификатор ресурса", max_length=10),
-                ),
-                (
-                    "requirements",
-                    models.CharField(
-                        help_text="код необходимого для того, чтобы ресурс мог стать активным",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="статус обработки заявки в системе заявок",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.",
-                        null=True,
-                    ),
-                ),
-                (
-                    "is_global",
-                    models.CharField(
-                        help_text="доступны ли документы для хранения в глобальном сервисе/нужна синхронизация",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "is_visible",
-                    models.CharField(
-                        help_text="доступна ли информация о наличии документов для планирования в цепочке с другими услугами в глобальном сервисе",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-                (
-                    "user_id",
-                    models.ForeignKey(
-                        help_text="уникальный идентификатор ответственного",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "resource",
-            },
-        ),
-        migrations.CreateModel(
-            name="ServiceType",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "codename",
-                    models.CharField(
-                        help_text="латинское наименование услуги в системе",
-                        max_length=255,
-                    ),
-                ),
-                (
-                    "caption",
-                    models.CharField(
-                        help_text="наименование услуги для отображения пользователю",
-                        max_length=255,
-                    ),
-                ),
-                (
-                    "description",
-                    models.TextField(blank=True, help_text="текстовое описание услуги"),
-                ),
-                (
-                    "requirements",
-                    models.CharField(
-                        help_text="код требований на основе вспомогательных таблиц-справочников",
-                        max_length=300,
-                    ),
-                ),
-                (
-                    "price_type",
-                    models.CharField(
-                        help_text="ценообразование - код допустимых вариантов или код параметров, принимаемых во внимание и способ их учета (по сути хорошо закодировать формулу)",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="активность на основе системы заявок", max_length=150
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text=" уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер."
-                    ),
-                ),
-                (
-                    "link_agreement",
-                    models.CharField(
-                        help_text="ссылка на договор в вики об оказании услуги данного типа (аренда, перевозка и тп)",
-                        max_length=400,
-                    ),
-                ),
-                (
-                    "is_global",
-                    models.CharField(
-                        help_text="доступно ли для планирования в цепочке с другими услугами в глобальном сервисе",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "is_visible",
-                    models.CharField(
-                        help_text="доступно ли для хранения в глобальном сервисе/нужна синхронизация данных",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "servicetype",
-            },
-        ),
-        migrations.CreateModel(
-            name="Service",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "requirements",
-                    models.CharField(
-                        help_text="код необходимого для того, чтобы ресурс мог стать активным",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.",
-                        null=True,
-                    ),
-                ),
-                (
-                    "price_alg",
-                    models.CharField(
-                        help_text="шаблон алгоритма расчета цены для оказываемой услуги",
-                        max_length=100,
-                    ),
-                ),
-                (
-                    "price_km",
-                    models.DecimalField(
-                        decimal_places=2,
-                        help_text="значение параметра стоимости 1км данного поставщика для данного шаблона услуги",
-                        max_digits=9,
-                    ),
-                ),
-                (
-                    "price_min",
-                    models.DecimalField(
-                        decimal_places=2,
-                        help_text="значение параметра стоимости 1мин данного поставщика для данного шаблона услуги",
-                        max_digits=9,
-                    ),
-                ),
-                (
-                    "price_amount",
-                    models.DecimalField(
-                        decimal_places=2,
-                        help_text="значение параметра стоимости 1 услуги данного поставщика для данного шаблона услуги",
-                        max_digits=9,
-                    ),
-                ),
-                (
-                    "service_status",
-                    models.CharField(
-                        help_text="статус спецификации типа услуги", max_length=150
-                    ),
-                ),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="статус обработки заявки в системе заявок",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "is_global",
-                    models.CharField(
-                        help_text="доступны ли документы для хранения в глобальном сервисе/нужна синхронизация",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "is_visible",
-                    models.CharField(
-                        help_text="доступна ли информация о наличии документов для планирования в цепочке с другими услугами в глобальном сервисе",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "id_provider",
-                    models.ForeignKey(
-                        help_text="идентификатор поставщика услуг",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.provider",
-                    ),
-                ),
-                (
-                    "resource_id",
-                    models.ForeignKey(
-                        help_text="ответственный за ресурс(не всегда)",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.resource",
-                    ),
-                ),
-                (
-                    "servicetype_id",
-                    models.ForeignKey(
-                        help_text="тип оказываемой услуги по классификатору услуг сервиса",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.servicetype",
-                    ),
-                ),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "service",
-            },
-        ),
-        migrations.CreateModel(
-            name="Relationship",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                ("neg_type", models.IntegerField()),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.",
-                        null=True,
-                    ),
-                ),
-                ("requirements", models.CharField(max_length=150)),
-                ("status", models.CharField(max_length=150)),
-                ("is_global", models.CharField(max_length=1)),
-                ("is_visible", models.CharField(max_length=1)),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-                (
-                    "user_id_who",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        related_name="user_id_who",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-                (
-                    "user_id_whom",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        related_name="user_id_whom",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "relationship",
-            },
-        ),
-        migrations.CreateModel(
-            name="Permissions",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "check_date",
-                    models.DateTimeField(help_text="timestamp проверки", null=True),
-                ),
-                (
-                    "id_permissions",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор определяющий наличие разрешения из множества в словаре - выданных пользователю/клиенту/аккаунту"
-                    ),
-                ),
-                (
-                    "check_level",
-                    models.CharField(
-                        help_text="тип проверки в соответствии с классификатором проверок.",
-                        max_length=10,
-                    ),
-                ),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="статус обработки заявки в системе заявок",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text=" уникальный идентификатор мета-сервиса, необходимый для синхронизации данных."
-                    ),
-                ),
-                (
-                    "is_global",
-                    models.CharField(
-                        help_text="доступна ли информация для хранения в глобальном сервисе/нужна синхронизация",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "is_visible",
-                    models.CharField(
-                        help_text="доступна ли информация о наличии разрешения для планирования в цепочке с другими услугами в глобальном сервисе",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "checked_by",
-                    models.ForeignKey(
-                        help_text="(check-level из классификатора платформы) - информация об уровне проверки. Проверка может быть проведена как платформой, так и мета-сервисом, так и партнером мета-сервиса, а может быть и никем (просто загружен). Указывается, так как достоверность проверки разная. Экзамен, проверенный только на низком уровне, не принимается во внимание как имеющийся до прохождения более высокоуровневой проверки.",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        related_name="checked_by_perm",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id заявки, по которой происходит проверка статуса relationship. State меняется только в результате изменений в заявке.",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-                (
-                    "user_id",
-                    models.ForeignKey(
-                        help_text="уникальный идентификатор пользователя/клиента/аккаунта, которым была пройдена проверка и получено разрешение",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        related_name="user_id_perm",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "permissions",
-            },
-        ),
-        migrations.CreateModel(
-            name="Orders",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                ("state", models.CharField(max_length=150)),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.",
-                        null=True,
-                    ),
-                ),
-                ("time_created", models.DateTimeField(auto_now_add=True)),
-                ("time_placed", models.DateTimeField()),
-                ("time_start", models.DateTimeField()),
-                ("time_finish_predicted", models.DateTimeField()),
-                ("time_finish_real", models.DateTimeField(null=True)),
-                ("ticket", models.IntegerField()),
-                ("predicted_price", models.FloatField()),
-                ("real_price", models.FloatField()),
-                ("is_global", models.CharField(max_length=1)),
-                ("is_visible", models.CharField(max_length=1)),
-                (
-                    "client_id",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.client",
-                    ),
-                ),
-                (
-                    "provider",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.provider",
-                    ),
-                ),
-                (
-                    "receiver",
-                    models.ForeignKey(
-                        help_text="уникальный идентификатор пользователя (конкретного клиентского аккаунта) являющегося владельцем данного документа",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        related_name="user_id",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-                (
-                    "service",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.service",
-                    ),
-                ),
-                (
-                    "service_type",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.servicetype",
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "orders",
-            },
-        ),
-        migrations.CreateModel(
-            name="Documents",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                ("check_date", models.DateTimeField(help_text="timestamp проверки")),
-                (
-                    "check_level",
-                    models.IntegerField(
-                        help_text="информация об уровне проверки. Документ может быть проверен как платформой, так и мета-сервисом, так и партнером мета-сервиса, а может быть и никем (просто загружен). Указывается, так как достоверность проверки разная. Документ, проверенный только на низком уровне, не принимается во внимание как имеющийся до прохождения более высокоуровневой проверки. Информацию об уровнях проверки можно посмотреть по словарю Requirements. В данной таблице хранится информация о наиболее высоком уровне проверки."
-                    ),
-                ),
-                (
-                    "expire_date",
-                    models.DateTimeField(
-                        help_text="срок окончания действия документа.", null=True
-                    ),
-                ),
-                (
-                    "id_metaservice",
-                    models.BigIntegerField(
-                        help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.",
-                        null=True,
-                    ),
-                ),
-                ("requirements", models.CharField(max_length=150)),
-                (
-                    "status",
-                    models.CharField(
-                        help_text="активность на основе системы заявок", max_length=150
-                    ),
-                ),
-                (
-                    "datalink",
-                    models.TextField(
-                        blank=True,
-                        help_text="адрес фактического размещения на физическом носителе, если информация настолько велика, что не может храниться внутри БД.",
-                    ),
-                ),
-                (
-                    "doc_type",
-                    models.CharField(
-                        help_text="тип документа (паспорт/паспорт 1 страница и т д) в соответствии с классификатором типов документов (см описание в Requirements)",
-                        max_length=150,
-                    ),
-                ),
-                (
-                    "is_global",
-                    models.CharField(
-                        help_text="доступны ли документы для хранения в глобальном сервисе/нужна синхронизация",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "is_visible",
-                    models.CharField(
-                        help_text="доступна ли информация о наличии документов для планирования в цепочке с другими услугами в глобальном сервисе",
-                        max_length=1,
-                    ),
-                ),
-                (
-                    "checked_by",
-                    models.ForeignKey(
-                        help_text="userid проверившего",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        related_name="checked_by_doc",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-                (
-                    "company_id",
-                    models.ForeignKey(
-                        help_text="идентификатор компании, к которой относится документ, если таковая есть (может не быть)",
-                        null=True,
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="metaservicesynced.company",
-                    ),
-                ),
-                (
-                    "ticket_status",
-                    models.ForeignKey(
-                        help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        to="tickets.task",
-                    ),
-                ),
-                (
-                    "user_id",
-                    models.ForeignKey(
-                        help_text="уникальный идентификатор пользователя (конкретного клиентского аккаунта) являющегося владельцем данного документа",
-                        on_delete=django.db.models.deletion.DO_NOTHING,
-                        related_name="user_id_doc",
-                        to=settings.AUTH_USER_MODEL,
-                    ),
-                ),
-            ],
-            options={
-                "db_table": "documents",
-            },
-        ),
-    ]

+ 0 - 0
metaservicesynced/migrations/__init__.py


+ 0 - 235
metaservicesynced/models.py

@@ -1,235 +0,0 @@
-from django.db import models
-from tickets.models import Task
-from SharixAdmin.models import SharixUser
-# Create your models here.
-
-
-class Company(models.Model):
-    legal_name = models.CharField(max_length=150, help_text="настоящее имя юридического лица")
-    repr_id = models.ForeignKey(SharixUser, on_delete=models.DO_NOTHING, help_text="уникальный идентификатор представителя компании. Это обязательно пользователь-провайдер определенного типа. То есть нельзя назначить ответственного, который не может быть ответственным.")
-    inn = models.CharField(max_length=10, unique=True, help_text="ИНН компании")
-    kpp = models.CharField(max_length=9,  help_text="КПП компании")
-    ogrn = models.CharField(max_length=13, help_text="ОГРН компании")
-    bank_name = models.CharField(max_length=150, help_text="Название банка с расчетным счетом")
-    bik = models.CharField(max_length=9, help_text="БИК компании")
-    ks = models.CharField(max_length=50, help_text="Корреспондентский счёт (счёт, открываемый банковской организацией в подразделении самого банка)")
-    rs = models.CharField(max_length=50, help_text="Расчетный счет")
-    address = models.CharField(max_length=150, help_text="Юридический адрес")
-    requirements = models.CharField(max_length=150, help_text="код необходимого (самый строгий) для того, чтобы ресурс мог стать активным. Оно вставляется автоматом, в соответствии с профилем метасервиса. Далее, если кому-то из партнеров или пользователей надо строже - применяется более строгий вариант на данную связь.")
-    status = models.CharField(max_length=150, help_text="статус обработки заявки в системе заявок")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, help_text="id заявки, по которой происходит проверка статуса relationship. State меняется только в результате изменений в заявке.")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер.")
-    is_global = models.CharField(max_length=1, help_text="доступно ли для хранения в глобальном сервисе/нужна синхронизация данных")
-    is_visible = models.CharField(max_length=1, help_text="доступно ли для планирования в цепочке с другими услугами в глобальном сервисе")
-
-    
-    class Meta:
-            db_table = "company"
-
-class Permissions(models.Model):
-    """
-    Разрешения - (проверки/экзамены). 
-    По смыслу это что-то вроде “документа на право что-то делать” - на данном этапе это ограничено метасервисом/платформой, 
-    при этом он может быть полностью цифровым (выданным платформой/сервисом).
-    """
-    
-    check_date = models.DateTimeField(null=True, help_text="timestamp проверки")
-    id_permissions = models.BigIntegerField(help_text="уникальный идентификатор определяющий наличие разрешения из множества в словаре - выданных пользователю/клиенту/аккаунту")
-    check_level = models.CharField(max_length=10, help_text="(check-level из классификатора платформы) - информация об уровне проверки. Проверка может быть проведена как платформой, так и мета-сервисом, так и партнером мета-сервиса, а может быть и никем (просто загружен). Указывается, так как достоверность проверки разная. Экзамен, проверенный только на низком уровне, не принимается во внимание как имеющийся до прохождения более высокоуровневой проверки.")
-    checked_by = models.ForeignKey(SharixUser, related_name="checked_by_perm", on_delete=models.DO_NOTHING, null=True, help_text="userid проверившего")
-    user_id = models.ForeignKey(SharixUser, related_name="user_id_perm", on_delete=models.DO_NOTHING, null=True, help_text="уникальный идентификатор пользователя/клиента/аккаунта, которым была пройдена проверка и получено разрешение")
-    status = models.CharField(max_length=150, help_text="статус обработки заявки в системе заявок")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, null=True, help_text="id заявки, по которой происходит проверка статуса relationship. State меняется только в результате изменений в заявке.")
-    id_metaservice = models.BigIntegerField(help_text=" уникальный идентификатор мета-сервиса, необходимый для синхронизации данных.")
-    is_global = models.CharField(max_length=1, help_text="доступна ли информация для хранения в глобальном сервисе/нужна синхронизация")
-    is_visible = models.CharField(max_length=1, help_text="доступна ли информация о наличии разрешения для планирования в цепочке с другими услугами в глобальном сервисе")
-
-    class Meta:
-        db_table = "permissions"
-
-class ServiceType(models.Model):
-    """
-    Перечень типов услуг
-    """
-
-    codename = models.CharField(max_length=255, help_text="латинское наименование услуги в системе")
-    caption = models.CharField(max_length=255, help_text="наименование услуги для отображения пользователю")
-    description = models.TextField(blank=True, help_text="текстовое описание услуги")
-    requirements = models.CharField(max_length=300, help_text="код требований на основе вспомогательных таблиц-справочников")
-    price_type = models.CharField(max_length=150, help_text="ценообразование - код допустимых вариантов или код параметров, принимаемых во внимание и способ их учета (по сути хорошо закодировать формулу)")
-    status = models.CharField(max_length=150, help_text="активность на основе системы заявок")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, null=True, help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.")
-    id_metaservice = models.BigIntegerField(help_text=" уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер.")
-    link_agreement = models.CharField(max_length=400, help_text="ссылка на договор в вики об оказании услуги данного типа (аренда, перевозка и тп)")
-    is_global = models.CharField(max_length=1, help_text="доступно ли для планирования в цепочке с другими услугами в глобальном сервисе")
-    is_visible = models.CharField(max_length=1, help_text="доступно ли для хранения в глобальном сервисе/нужна синхронизация данных")
-
-    class Meta:
-        db_table = "servicetype"
-
-class Provider(models.Model):
-    """
-    Provider – единица описания поставщика услуг/ответственного лица за определенный ресурс (например, машину). 
-    По сути - это надстройка к клиентскому аккаунту, иллюстрирующая, что данный пользователь может выступать не только в роли потребителя. 
-    То есть, по тому, какие “провайдеры” находятся по идентификатору пользователя - можно установить конкретный список услуг данного пользователя.
-    """
-
-    type = models.CharField(max_length=150, help_text="тип поставщика (партнер/ответственное лицо/поставщик услуг). Смысл такой - провайдер это статус пользователя, который, в зависимости от применения, может нести разный смысл и подразумевает под собой какой-то тип действия. Обычные исполнители - это провайдеры услуг (код 3). Ответственные за какое-то имущество, которые сдают его в аренду - это тоже провайдеры (код 2). Ответственные за набор услуг перед метасервисом (фактически - назначенные админы) - это провайдеры-партнеры (код 1)")
-    company_id = models.ForeignKey(Company, on_delete=models.DO_NOTHING, null=True, help_text="уникальный идентификатор компании, от лица которой выступает провайдер. Смысл такой - ответственны могут быть только одушевленные лица, компании - не одушевленные. Все услуги предоставляются через компании-партнеры, самозанятые или ИП являются единицами таких компаний.")
-    user_id = models.ForeignKey(SharixUser, on_delete=models.DO_NOTHING, null=True, help_text="уникальный идентификатор конкретного пользователя системы (meta-user), который будет оказывать услугу. Один пользователь может быть провайдером нескольких услуг. Статус провайдера означает, что с данным пользователем может быть установлена связь, как с исполнителем.")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер.")
-    requirements = models.CharField(max_length=300, help_text="требования для того, чтобы можно было предоставлять услуги любые в этом метасервисе в целом (самые строгие)")
-    status = models.CharField(max_length=150, help_text="статус пользователя в системе относительно прохождения проверок (activity_status) (может быть active только в том случае, если ticket, влияющий на статус - закрыт.")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, null=True, help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.")
-    location_type = models.CharField(max_length=300, help_text="статическая или динамическая локация оказания услуги. Если статическая, а исполнитель находится существенно за пределами локации - то тогда статус автоматом оффлайн для приема новых заявок.")
-    default_location = models.CharField(max_length=300, help_text="локация по умолчанию для объекта.")
-    is_global = models.CharField(max_length=1, help_text="(аккаунт поставщика услуг) – доступен для планирования в цепочке с другими услугами в глобальном сервисе")
-    is_visible = models.CharField(max_length=1, help_text="(аккаунт поставщика услуг) – доступен для хранения в  глобальном сервисе/необходима синхронизация")
-
-    class Meta:
-        db_table = "provider"
-        
-
-
-class Resource(models.Model):
-    """
-    Resource/Список ресурсов – автомобили/дома/объекты сервиса
-    """
-
-    type_id = models.CharField(max_length=10, help_text="определение типа ресурса по его уникальному идентификатору в соответствии с классификатором")
-    user_id = models.ForeignKey(SharixUser, on_delete=models.DO_NOTHING, help_text="уникальный идентификатор ответственного (за состояние, доступность и так далее - то есть для договора) пользователя - идентификатор провайдера, по которому восстанавливается конкретный пользовательский аккаунт")
-    requirements = models.CharField(max_length=150, help_text="код необходимого (самый строгий) для того, чтобы ресурс мог стать активным")
-    status = models.CharField(max_length=150, help_text="статус ресурса в системе относительно прохождения проверок (activity_status) (может быть active только в том случае, если ticket, влияющий на статус - закрыт.")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер.")
-    is_global = models.CharField(max_length=1, help_text="доступны ли данные (по услугам или ресурсам?) для хранения в глобальном сервисе/необходима синхронизация")
-    is_visible = models.CharField(max_length=1, help_text="доступно ли для планирования в цепочке с другими услугами в глобальном сервисе")
-
-    
-    class Meta:
-        db_table = "resource"
-
-class Service(models.Model):
-    """
-    service - спецификация услуги каждого конкретного поставщика 
-    (например, в рамках сервиса многие могут предоставлять услуги перевозки, 
-    но конкретный шаблон с конкретным тарифом относится к отдельному перевозчику)
-    """
-
-    servicetype_id = models.ForeignKey(ServiceType, on_delete=models.DO_NOTHING, help_text="тип оказываемой услуги по классификатору услуг сервиса")
-    id_provider = models.ForeignKey(Provider,on_delete=models.DO_NOTHING, help_text="уникальный идентификатор поставщика услуг (фактически определяет, какой пользователь будет оказывать услугу)")
-    resource_id = models.ForeignKey(Resource, on_delete=models.DO_NOTHING, null=True ,help_text="ответственный за ресурс(не всегда). так как ресурсы сами услугу оказать не могут, а также один ресурс может быть представлен в виде разных услуг, то фактически с точки зрения смысла системы ресурс - это как неодушевленный пользователь. Без провайдера, который с его помощью оказывает услугу - никуда. Поле остается пустым, если сервис не предусматривает использование услуг. Стоит обратить внимание, что это не обязательно ответственный за ресурс. Например, за состояние автомобиля может быть ответственен пользователь (он и указывается в таблице со свойствами ресурса), а услугу доступа или перевозки может оказывать иное лицо.")
-    requirements = models.CharField(max_length=150, help_text="код необходимого (самый строгий) для того, чтобы ресурс мог стать активным. Оно вставляется автоматом, в соответствии с профилем метасервиса. Далее, если кому-то из партнеров или пользователей надо строже - применяется более строгий вариант на данную связь.")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер.")
-    price_alg = models.CharField(max_length=100, help_text="шаблон алгоритма расчета цены для оказываемой услуги (по этой переменной определяется, какую функцию для расчета цены вызывать)")
-    price_km = models.DecimalField(max_digits=9, decimal_places=2, help_text="значение параметра стоимости 1км данного поставщика для данного шаблона услуги")
-    price_min =  models.DecimalField(max_digits=9, decimal_places=2, help_text="значение параметра стоимости 1мин данного поставщика для данного шаблона услуги")
-    price_amount =  models.DecimalField(max_digits=9, decimal_places=2, help_text="значение параметра стоимости 1 услуги данного поставщика для данного шаблона услуги")
-    service_status = models.CharField(max_length=150, help_text="статус спецификации типа услуги, принимает значения Online, Offline, Preorder with Gap. Online/offline выставляются по проверке параметров и желанию пользователя (например, если пользователь переключает себя online, но по какой-то причине ему такую услугу оказывать запрещено - оно не переключится, то есть надо перед сменой значения этого поля всегда запускать проверку)")
-    status = models.CharField(max_length=150, help_text="статус обработки заявки в системе заявок. активность на основе системы заяво")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.")
-    is_global = models.CharField(max_length=1, help_text="доступны ли документы для хранения в глобальном сервисе/нужна синхронизация")
-    is_visible = models.CharField(max_length=1, help_text="доступна ли информация о наличии документов для планирования в цепочке с другими услугами в глобальном сервисе")
-    
-    class Meta:
-        db_table = "service"
-
-
-class Documents(models.Model):
-    """
-    Documents - это одна таблица со всеми документами.
-    Вообще в концепции предполагалось, что таких таблиц должно быть много под каждый тип для удобства поиска. 
-    То есть отдельно таблица с паспортами, отдельно с правами, отдельно с какими-нибудь разрешениями и так далее. 
-    Что пока непонятно - документов может быть много разных.
-    """
-
-    check_date = models.DateTimeField(help_text="timestamp проверки")
-    check_level = models.IntegerField(help_text="информация об уровне проверки. Документ может быть проверен как платформой, так и мета-сервисом, так и партнером мета-сервиса, а может быть и никем (просто загружен). Указывается, так как достоверность проверки разная. Документ, проверенный только на низком уровне, не принимается во внимание как имеющийся до прохождения более высокоуровневой проверки. Информацию об уровнях проверки можно посмотреть по словарю Requirements. В данной таблице хранится информация о наиболее высоком уровне проверки.")
-    expire_date = models.DateTimeField(null=True, help_text="срок окончания действия документа.")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.")
-    requirements = models.CharField(max_length=150)
-    status = models.CharField(max_length=150, help_text="активность на основе системы заявок")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.")
-    datalink = models.TextField(blank=True, help_text="адрес фактического размещения на физическом носителе, если информация настолько велика, что не может храниться внутри БД.")
-    doc_type = models.CharField(max_length=150, help_text="тип документа (паспорт/паспорт 1 страница и т д) в соответствии с классификатором типов документов (см описание в Requirements)")
-    user_id = models.ForeignKey(SharixUser, related_name="user_id_doc", on_delete=models.DO_NOTHING, help_text="уникальный идентификатор пользователя (конкретного клиентского аккаунта) являющегося владельцем данного документа")
-    company_id = models.ForeignKey(Company, on_delete=models.DO_NOTHING, null=True, help_text="идентификатор компании, к которой относится документ, если таковая есть (может не быть)")
-    is_global = models.CharField(max_length=1, help_text="доступны ли документы для хранения в глобальном сервисе/нужна синхронизация")
-    is_visible = models.CharField(max_length=1, help_text="доступна ли информация о наличии документов для планирования в цепочке с другими услугами в глобальном сервисе")
-    checked_by = models.ForeignKey(SharixUser, related_name="checked_by_doc", on_delete=models.DO_NOTHING, null=True, help_text="userid проверившего")
-
-
-    class Meta:
-        db_table = "documents"
-
-
-class Client(models.Model):
-    """
-    Client - это таблица с клиентами. Клиент/пользователь/аккаунт 
-    в системе, который по логике получает услугу.
-    """
-    user = models.ForeignKey(SharixUser, on_delete=models.DO_NOTHING, help_text="пользователь, которому соответствует роль клиента")
-    requirements = models.CharField(max_length=150, help_text="требования для того, чтобы можно было получать услуги как клиент")
-    status = models.CharField(max_length=150, help_text="активность на основе системы заявок")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, help_text="id последнего актуального тикета, касающийся статуса. Если он меняет статус на закрытый - вызывается проверка, которая смотрит, нет ли другого открытого по пользователю.")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.")
-    is_global = models.CharField(max_length=1, help_text="доступно ли для хранения в глобальном сервисе/необходима синхронизация")
-    is_visible = models.CharField(max_length=1, help_text="доступно ли для планирования в цепочке с другими услугами в глобальном сервисе")
-
-
-    class Meta:
-        db_table = "client"
-
-
-class Orders(models.Model):
-    """
-    Orders - таблица с заказами
-    """
-
-
-    service = models.ForeignKey(Service, on_delete=models.DO_NOTHING, help_text="спецификатор услуги провайдера, нужен для установления цены (id_service - уникальный идентификатор шаблона услуги, необходим для установления цены и исполнителей.")
-    service_type = models.ForeignKey(ServiceType, on_delete=models.DO_NOTHING, help_text="тип заказа по классификатору услу")
-    state = models.CharField(max_length=150, help_text="текущий статус заказа из возможных на платформе")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Если при синхронизации возникает конфликт (несовместимость) с другим сервисом, предлагается или форсировать изменения везде (если возможно), либо is_global выставляется как false.")
-    provider = models.ForeignKey(Provider, on_delete=models.DO_NOTHING, help_text="уникальный идентификатор поставщика услуги/аккаунта, который оказывает услугу. Если несколько провайдеров собираются мета-сервисом в цепочку, где на уровне связи с клиентом нельзя установить одно ответственное лицо, то указывается вспомогательный мета-провайдер сервиса, и это означает, что мета-сервис несет ответственность перед пользователем за сборку услуги воедино.") 
-    receiver = models.ForeignKey(SharixUser, related_name="user_id", on_delete=models.DO_NOTHING, help_text="пользователь/аккаунт, который принимает оказываемые услуги")
-    client_id = models.ForeignKey(Client, on_delete=models.DO_NOTHING, help_text="клиент/аккаунт, который оплачивает все оказанные услуги") 
-    time_created = models.DateTimeField(auto_now_add=True, help_text="время создания заказа")
-    time_placed = models.DateTimeField(help_text="время размещения заказа")
-    time_start = models.DateTimeField(help_text="время начала оказания услуги")
-    time_finish_predicted = models.DateTimeField(help_text="предварительное/расчетное время до окончания оказания услуги")
-    time_finish_real = models.DateTimeField(null=True, help_text="фактическое время окончания (точное установленное время)")
-    ticket = models.IntegerField(help_text="")
-    predicted_price = models.FloatField(help_text="расчетная цена с учетом тарифа поставщика услуг")
-    real_price = models.FloatField(help_text="цена с учетом тарифа поставщика услуг по факту оказания услуги")
-    is_global = models.CharField(max_length=1, help_text="доступна ли информация по заказу для хранения в глобальном сервисе/нужна синхронизация данных. Если is_global = false, то и is_visible для заказа и вглубь по цепочке для всех исполнителей и ресурсов - тоже false.")
-    is_visible = models.CharField(max_length=1, help_text="доступна ли информация по заказу (время, место) для планирования иных цепочек. Если нет, то все действующие исполнители и ресурсы считаются занятыми на неопределенное время, пока не завершится заказ. Если да - то ресурсы могут использоваться для построения цепочек после планируемого времени завершения, с учетом места.")
-
-
-    class Meta:
-        db_table = "orders"
-
-
-
-
-#hi
-class Relationship(models.Model):
-    """
-    Relationship - описание связей 
-    (желательных - как имеющиеся договорные отношения, 
-    и нежелательных - как пожелание любой из сторон)
-    """
-    user_id_who = models.ForeignKey(SharixUser, related_name="user_id_who", on_delete=models.DO_NOTHING, help_text="уникальный идентификатор инициатора договорных отношений")
-    user_id_whom = models.ForeignKey(SharixUser,  related_name="user_id_whom", on_delete=models.DO_NOTHING, help_text=" уникальный идентификатор того с кем связываются")
-    neg_type = models.IntegerField(help_text="тип договорных отношений по его уникальному идентификатору")
-    id_metaservice = models.BigIntegerField(null=True, help_text="уникальный идентификатор мета-сервиса, необходимый для синхронизации данных. Один и тот же провайдер может быть для нескольких мета-сервисов, соответственно если происходят изменения в одном, то либо форсируется изменение во всех (если возможно), либо снимается is_global. Соответственно при изменении is_global в true должно происходить согласование с остальными копиями в других сервисах. Нужен в том числе для того, чтобы выяснять, в каких еще сервисах есть этот провайдер.")
-    requirements = models.CharField(max_length=150, help_text="код необходимого (самый строгий) для того, чтобы ресурс мог стать активным. Оно вставляется автоматом, в соответствии с профилем метасервиса. Далее, если кому-то из партнеров или пользователей надо строже - применяется более строгий вариант на данную связь.")
-    status = models.CharField(max_length=150, help_text="(статус обработки заявки в системе заявок)")
-    ticket_status = models.ForeignKey(Task, on_delete=models.DO_NOTHING, help_text="id заявки, по которой происходит проверка статуса relationship. State меняется только в результате изменений в заявке.")
-    is_global = models.CharField(max_length=1, help_text="установленный тип договорных отношений между клиентами/пользователями/аккаунтами доступен для хранения в глобальном сервисе/нужна синхронизация")
-    is_visible = models.CharField(max_length=1, help_text="установленный тип договорных отношений между клиентами/пользователями/аккаунтами, доступен для планирования в цепочке с другими услугами в глобальном сервисе")
-
-
-    class Meta:
-        db_table = "relationship"
-

+ 0 - 8
metaservicesynced/serializer.py

@@ -1,8 +0,0 @@
-from rest_framework import serializers
-from .models import *
-from django.contrib.auth.models import *
-
-class DocumentsSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Documents
-        exclude = ["id"]

+ 0 - 3
metaservicesynced/tests.py

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

+ 0 - 12
metaservicesynced/urls.py

@@ -1,12 +0,0 @@
-from metaservicesynced.apiview import *
-from rest_framework import routers
-from django.urls import path, include
-
-from metaservicesynced.views import *
-
-router = routers.SimpleRouter()
-router.register(r'documents', DocumentsMVS)
-
-urlpatterns = [  
-    path('api/', include(router.urls), name="documents"),
-]

+ 0 - 5
metaservicesynced/views.py

@@ -1,5 +0,0 @@
-from django.shortcuts import render
-from metaservicesynced.models import *
-
-# Create your views here.
-

+ 0 - 5
tickets/.gitignore

@@ -1,5 +0,0 @@
-# Python
-__pycache__/
-*.py[cod]
-*$py.class
-00*.py

+ 0 - 77
tickets/README.md

@@ -1,77 +0,0 @@
-# ShariX Open Tickets
-
-Ticketing system implemented as a Django application.
-
-## Installation
-
-1) Download or clone repository
-
-```bash
-git clone -b tickets_module http://git.sharix-app.org/ShariX_Open/sharix-open-tickets.git tickets
-```
-
-2) Install required dependencies in project settings
-
-```python
-INSTALLED_APPS = [
-    ...
-    'tickets.apps.ticketsConfig',
-    'django.contrib.sites',
-    ...
-]
-```
-
-3) Delete the migration files in the **migrations** folder (everything except __init__.py)
-
-4) Install required libraries (don't forget to install and activate virtual space)
-
-```powershell
-pip install -r tickets/requirements.txt
-```
-
-Start test the server:
-
-```bash
-python manage.py runserver 8000
-```
-
-If the port has not been selected, it is 8000 by default. If selected port is busy, use another one (for example, try increasing port number by 1 until the server starts). A link to the website should appear in the terminal.
-
-## Settings
-
-Optional configuration params, which can be added to your project settings:
-
-```python
-# Restrict access to ALL tickets lists/views to `is_staff` users.
-# If False or unset, all users can see all views (but more granular permissions are still enforced
-# within views, such as requiring staff for adding and deleting lists).
-TICKETS_STAFF_ONLY = True
-
-# If you use the "public" ticket filing option, to whom should these tickets be assigned?
-# Must be a valid username in your system. If unset, unassigned tickets go to "Anyone."
-TICKETS_DEFAULT_ASSIGNEE = 'johndoe'
-
-# If you use the "public" ticket filing option, to which list should these tickets be saved?
-# Defaults to first list found, which is probably not what you want!
-TICKETS_DEFAULT_LIST_SLUG = 'tickets'
-
-# If you use the "public" ticket filing option, to which *named URL* should the user be
-# redirected after submitting? (since they can't see the rest of the ticket system).
-# Defaults to "/"
-TICKETS_PUBLIC_SUBMIT_REDIRECT = 'dashboard'
-
-# Enable or disable file attachments on Tasks
-# Optionally limit list of allowed filetypes
-TICKETS_ALLOW_FILE_ATTACHMENTS = True
-TICKETS_ALLOWED_FILE_ATTACHMENTS = [".jpg", ".gif", ".csv", ".pdf", ".zip"]
-TICKETS_MAXIMUM_ATTACHMENT_SIZE = 5000000  # In bytes
-
-# Additional classes the comment body should hold.
-# Adding "text-monospace" makes comment monospace
-TICKETS_COMMENT_CLASSES = []
-
-# The following two settings are relevant only if you want TICKETS to track a support mailbox -
-# see Mail Tracking below.
-TICKETS_MAIL_BACKENDS
-TICKETS_MAIL_TRACKERS
-```

+ 0 - 0
tickets/__init__.py


+ 0 - 48
tickets/admin.py

@@ -1,48 +0,0 @@
-from django.contrib import admin
-from tickets.models import Attachment, Comment, Task, TaskList, TicketType
-from tickets.admin_utils import *
-from django.contrib.auth.models import Group
-from django.contrib.auth.admin import GroupAdmin
-
-
-@admin.register(Task)
-class TaskAdmin(admin.ModelAdmin):
-    list_display = ("title", "task_list", "status", "priority", "due_date")
-    list_filter = ("task_list",)
-    ordering = ("priority",)
-    search_fields = ("title",)
-
-@admin.register(TaskList)
-class TaskListAdmin(admin.ModelAdmin):
-    actions = [add_default_tickets_list_admin]
-
-@admin.register(TicketType)
-class TicketTypeAdmin(admin.ModelAdmin):
-    actions = [add_default_ticketstype_admin]
-
-admin.site.unregister(Group)
-@admin.register(Group)
-class GroupsAdmin(admin.ModelAdmin):
-    actions = [add_default_group_admin]
-    search_fields = ("name",)
-    ordering = ("name",)
-    filter_horizontal = ("permissions",)
-
-    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
-        if db_field.name == "permissions":
-            qs = kwargs.get("queryset", db_field.remote_field.model.objects)
-            kwargs["queryset"] = qs.select_related("content_type")
-        return super().formfield_for_manytomany(db_field, request=request, **kwargs)
-
-class CommentAdmin(admin.ModelAdmin):
-    list_display = ("author", "date", "snippet")
-
-
-class AttachmentAdmin(admin.ModelAdmin):
-    list_display = ("task", "added_by", "timestamp", "file")
-    autocomplete_fields = ["added_by", "task"]
-
-
-admin.site.register(Comment, CommentAdmin)
-admin.site.add_action(export_to_csv)
-admin.site.register(Attachment)

+ 0 - 77
tickets/admin_utils.py

@@ -1,77 +0,0 @@
-from tickets.models import *
-from django.contrib.contenttypes.models import ContentType
-from django.contrib.auth.models import Group, Permission
-from django.http import HttpResponse
-import csv
-
-def add_perm(group, permissions, contenttype):
-    tct = ContentType.objects.get_for_model(contenttype)
-    for i in permissions:
-        group.permissions.add(Permission.objects.get(codename=i, content_type=tct))
-
-def export_to_csv(modeladmin, request, queryset):
-    meta = modeladmin.model._meta
-    field_names = [field.name for field in meta.fields]
-    #field_show_names = [field.verbose_name for field in meta.fields]
-    response = HttpResponse(content_type='text/csv')
-    response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
-    writer = csv.writer(response)
-
-    writer.writerow(field_names)
-    #writer.writerow(field_show_names)
-    for obj in queryset:
-        row = writer.writerow([getattr(obj, field) for field in field_names])
-
-    return response
-
-def add_default_ticketstype():
-    if not TicketType.objects.filter(name="SERVICE_REQUEST"):
-        TicketType.objects.create(name="SERVICE_REQUEST", life_cycle="210-211-251,211-212-220-238-249,212-221-229-238-249,221-222-238-249,220-211-238-249,229-211-251,222-231-238-249,231-241-238-249,238-231-239-211-212-221-220-222-249,239-231-239-211-212-221-220")
-        TicketType.objects.create(name="ST_REQUEST", life_cycle="111-121-149-159,110-121-149-159,121-131-149-159,131-141-149,141-151-110,149-151-110,159,151")
-        TicketType.objects.create(name="NEG_REQUEST", life_cycle="420-421-459,421,459")
-        TicketType.objects.create(name="ACCESS_REQUEST", life_cycle="320-321-359,321,359")
-
-def add_default_group():
-    if not Group.objects.filter(name="meta-user"):
-        #Meta user
-        group = Group.objects.create(name="meta-user")
-        add_perm(group, ['add_task', 'change_task', 'view_task'], Task)
-        #Platform admin
-        group = Group.objects.create(name="platform-admin")
-        add_perm(group, ['add_attachment', 'view_attachment'], Attachment)
-        add_perm(group, [ 'add_comment', 'view_comment'], Comment)
-        add_perm(group, [ 'add_task', 'change_task', 'view_task'], Task)
-        group = Group.objects.create(name="platform-supervisor")
-        add_perm(group, ['add_attachment', 'view_attachment'], Attachment)
-        add_perm(group, [ 'add_comment', 'view_comment'], Comment)
-        add_perm(group, [ 'add_task', 'change_task', 'view_task'], Task)
-        group = Group.objects.create(name="platform-support")
-        add_perm(group, [ 'add_comment', 'view_comment'], Comment)
-        add_perm(group, [ 'add_task', 'change_task', 'view_task'], Task)
-        group = Group.objects.create(name="platform-techsupport")
-        add_perm(group, ['add_attachment', 'view_attachment'], Attachment)
-        add_perm(group, [ 'add_comment', 'view_comment'], Comment)
-        add_perm(group, [ 'add_task', 'change_task', 'view_task'], Task)
-
-def add_default_tickets_list(request):
-    if not TaskList.objects.filter(name="Заявки клиентов"):
-        if not Group.objects.filter(name="TestGroup"):
-            group = Group.objects.create(name="TestGroup")
-            group.user_set.add(request.user)
-        else:
-            group = Group.objects.get(pk=1)
-        TaskList.objects.create(name="Заявки клиентов", slug="customer-applications", group=group)
-
-def add_default_ticketstype_admin(modeladmin, request, queryset):
-    add_default_ticketstype()
-
-def add_default_group_admin(modeladmin, request, queryset):
-    add_default_group()
-
-def add_default_tickets_list_admin(modeladmin, request, queryset):
-    add_default_tickets_list(request)
-
-add_default_tickets_list_admin.short_description = "Создать значения по умолчанию"
-add_default_ticketstype_admin.short_description = "Создать значения по умолчанию"
-add_default_group_admin.short_description = "Создать значения по умолчанию"
-export_to_csv.short_description = "Экспортировать в CSV"

+ 0 - 30
tickets/apiviews.py

@@ -1,30 +0,0 @@
-from tickets.serializer import *
-from rest_framework import viewsets, permissions, exceptions
-from rest_framework.authentication import TokenAuthentication
-from rest_framework.decorators import action
-from tickets.models import *
-
-class TaskMVS(viewsets.ModelViewSet):
-    queryset = Task.objects.all()
-    serializer_class = TaskSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-class TaskListMVS(viewsets.ModelViewSet):
-    queryset = TaskList.objects.all()
-    serializer_class = TaskListSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-class TicketTypeMVS(viewsets.ModelViewSet):
-    queryset = TicketType.objects.all()
-    serializer_class = TicketTypeSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-class CommentMVS(viewsets.ModelViewSet):
-    queryset = Comment.objects.all()
-    serializer_class = CommentSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-class AttachmentMVS(viewsets.ModelViewSet):
-    queryset = Attachment.objects.all()
-    serializer_class = AttachmentSerializer
-    permission_classes = [permissions.IsAuthenticated]

+ 0 - 7
tickets/apps.py

@@ -1,7 +0,0 @@
-from django.apps import AppConfig
-
-
-class ticketsConfig(AppConfig):
-    default_auto_field = "django.db.models.BigAutoField"
-    name = "tickets"
-    verbose_name = "TICKETS"

+ 0 - 22
tickets/defaults.py

@@ -1,22 +0,0 @@
-from django.conf import settings
-
-hash = {
-    "TICKETS_ALLOW_FILE_ATTACHMENTS": True,
-    "TICKETS_COMMENT_CLASSES": [],
-    "TICKETS_DEFAULT_ASSIGNEE": None,
-    "TICKETS_LIMIT_FILE_ATTACHMENTS": [".jpg", ".gif", ".png", ".csv", ".pdf", ".zip"],
-    "TICKETS_MAXIMUM_ATTACHMENT_SIZE": 5000000,
-    "TICKETS_PUBLIC_SUBMIT_REDIRECT": "/",
-    "TICKETS_STAFF_ONLY": True,
-}
-
-
-def defaults(key: str):
-    """Try to get a setting from project settings.
-    If empty or doesn't exist, fall back to a value from defaults hash."""
-
-    if hasattr(settings, key):
-        val = getattr(settings, key)
-    else:
-        val = hash.get(key)
-    return val

+ 0 - 64
tickets/forms.py

@@ -1,64 +0,0 @@
-from django import forms
-from django.contrib.auth.models import Group
-from django.forms import ModelForm
-from tickets.models import Task, TaskList, TicketType
-from django.conf import settings
-from SharixAdmin.models import SharixUser
-
-
-class ListCreateForm(ModelForm):
-    def __init__(self, user, *args, **kwargs):
-        super(ListCreateForm, self).__init__(*args, **kwargs)
-        self.fields["group"].queryset = Group.objects.filter(user=user)
-        self.fields["group"].widget.attrs = {
-            "id": "id_group",
-            "class": "custom-select mb-3",
-            "name": "group",
-        }
-        self.fields["name"].widget.attrs = {
-            "id": "id_name",
-            "class": "form-control",
-            "name": "group",
-        }
-
-    class Meta:
-        model = TaskList
-        exclude = ["slug"]
-
-
-class TicketForm(ModelForm):
-    def __init__(self, user, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        task_list = kwargs.get("initial").get("task_list")
-        members = task_list.group.user_set.all()
-        #print(user)
-        #print(task_list.group)
-        #members = SharixUser.objects.filter(groups__name=task_list.group)
-        #print(members)
-        self.fields["assigned_to"].queryset = members
-        self.fields["assigned_to"].label_from_instance = lambda obj: "%s (%s)" % (
-            obj.get_full_name(),
-            obj.username,
-        )
-        self.fields["assigned_to"].widget.attrs = {"class": "custom-select"}
-        self.fields["type"].widget.attrs = {"class": "custom-select"}
-        self.fields["title"].widget.attrs = {"class": "form-control"}
-        self.fields["note"].widget.attrs = {"class": "form-control"}
-        
-        if kwargs.get("initial").get("type_disabled") == True:
-            self.fields["type"].disabled = True
-
-    due_date = forms.DateField(widget=forms.DateInput(attrs={"type": "date", "class": "form-control"}))
-
-    def clean_created_by(self):
-        """Keep the existing created_by regardless of anything coming from the submitted form.
-        If creating a new task, then created_by will be None, but we set it before saving."""
-        return self.instance.created_by
-
-    class Meta:
-        model = Task
-        fields = ["type", "title", "note", "due_date", "assigned_to", "created_by"]
-
-
-class SearchForm(forms.Form):
-    q = forms.CharField(widget=forms.widgets.TextInput(attrs={"size": 35}))

+ 0 - 0
tickets/mail/__init__.py


+ 0 - 9
tickets/mail/consumers/__init__.py

@@ -1,9 +0,0 @@
-def tracker_consumer(**kwargs):
-    def tracker_factory(producer):
-        # the import needs to be delayed until call to enable
-        # using the wrapper in the django settings
-        from .tracker import tracker_consumer
-
-        return tracker_consumer(producer, **kwargs)
-
-    return tracker_factory

+ 0 - 171
tickets/mail/consumers/tracker.py

@@ -1,171 +0,0 @@
-import re
-import logging
-
-from email.charset import Charset as EMailCharset
-from django.db import transaction
-from django.db.models import Count
-from django.contrib.auth import get_user_model
-from django.conf import settings
-from html2text import html2text
-from email.utils import parseaddr
-from tickets.models import Comment, Task, TaskList
-
-logger = logging.getLogger(__name__)
-
-
-def part_decode(message):
-    charset = ("ascii", "ignore")
-    email_charset = message.get_content_charset()
-    if email_charset:
-        charset = (EMailCharset(email_charset).input_charset,)
-
-    body = message.get_payload(decode=True)
-    return body.decode(*charset)
-
-
-def message_find_mime(message, mime_type):
-    for submessage in message.walk():
-        if submessage.get_content_type() == mime_type:
-            return submessage
-    return None
-
-
-def message_text(message):
-    text_part = message_find_mime(message, "text/plain")
-    if text_part is not None:
-        return part_decode(text_part)
-
-    html_part = message_find_mime(message, "text/html")
-    if html_part is not None:
-        return html2text(part_decode(html_part))
-
-    # tickets: find something smart to do when no text if found
-    return ""
-
-
-def format_task_title(format_string, message):
-    return format_string.format(subject=message["subject"], author=message["from"])
-
-
-DJANGO_TICKETS_THREAD = re.compile(r"<thread-(\d+)@django-tickets>")
-
-
-def parse_references(task_list, references):
-    related_messages = []
-    answer_thread = None
-    for related_message in references.split():
-        logger.info("checking reference: %r", related_message)
-        match = re.match(DJANGO_TICKETS_THREAD, related_message)
-        if match is None:
-            related_messages.append(related_message)
-            continue
-
-        thread_id = int(match.group(1))
-        new_answer_thread = Task.objects.filter(task_list=task_list, pk=thread_id).first()
-        if new_answer_thread is not None:
-            answer_thread = new_answer_thread
-
-    if answer_thread is None:
-        logger.info("no answer thread found in references")
-    else:
-        logger.info("found an answer thread: %s", str(answer_thread))
-    return related_messages, answer_thread
-
-
-def insert_message(task_list, message, priority, task_title_format):
-    if "message-id" not in message:
-        logger.warning("missing message id, ignoring message")
-        return
-
-    if "from" not in message:
-        logger.warning('missing "From" header, ignoring message')
-        return
-
-    if "subject" not in message:
-        logger.warning('missing "Subject" header, ignoring message')
-        return
-
-    logger.info(
-        "received message:\t"
-        f"[Subject: {message['subject']}]\t"
-        f"[Message-ID: {message['message-id']}]\t"
-        f"[References: {message['references']}]\t"
-        f"[To: {message['to']}]\t"
-        f"[From: {message['from']}]"
-    )
-
-    # Due to limitations in MySQL wrt unique_together and TextField (grrr),
-    # we must use a CharField rather than TextField for message_id.
-    # In the unlikeley event that we get a VERY long inbound
-    # message_id, truncate it to the max_length of a MySQL CharField.
-    original_message_id = message["message-id"]
-    message_id = (
-        (original_message_id[:252] + "...")
-        if len(original_message_id) > 255
-        else original_message_id
-    )
-    message_from = message["from"]
-    text = message_text(message)
-
-    related_messages, answer_thread = parse_references(task_list, message.get("references", ""))
-
-    # find the most relevant task to add a comment on.
-    # among tasks in the selected task list, find the task having the
-    # most email comments the current message references
-    best_task = (
-        Task.objects.filter(task_list=task_list, comment__email_message_id__in=related_messages)
-        .annotate(num_comments=Count("comment"))
-        .order_by("-num_comments")
-        .only("id")
-        .first()
-    )
-
-    # if no related comment is found but a thread message-id
-    # (generated by django-tickets) could be found, use it
-    if best_task is None and answer_thread is not None:
-        best_task = answer_thread
-
-    with transaction.atomic():
-        if best_task is None:
-            best_task = Task.objects.create(
-                priority=priority,
-                title=format_task_title(task_title_format, message),
-                task_list=task_list,
-                created_by=match_user(message_from),
-            )
-        logger.info("using task: %r", best_task)
-
-        comment, comment_created = Comment.objects.get_or_create(
-            task=best_task,
-            email_message_id=message_id,
-            defaults={"email_from": message_from, "body": text},
-            author=match_user(message_from), # tickets: Write test for this
-        )
-        logger.info("created comment: %r", comment)
-
-
-def tracker_consumer(
-    producer, group=None, task_list_slug=None, priority=1, task_title_format="[MAIL] {subject}"
-):
-    task_list = TaskList.objects.get(group__name=group, slug=task_list_slug)
-    for message in producer:
-        try:
-            insert_message(task_list, message, priority, task_title_format)
-        except Exception:
-            # ignore exceptions during insertion, in order to avoid
-            logger.exception("got exception while inserting message")
-
-
-def match_user(email):
-    """ This function takes an email and checks for a registered user."""
-
-    if not settings.TICKETS_MAIL_USER_MAPPER:
-        user = None
-    else:
-        try:
-            # Find the first user that matches the email
-            user = get_user_model().objects.get(email=parseaddr(email)[1])
-        except get_user_model().DoesNotExist:
-            user = None
-
-    return user

+ 0 - 27
tickets/mail/delivery.py

@@ -1,27 +0,0 @@
-import importlib
-
-
-def _declare_backend(backend_path):
-    backend_path = backend_path.split(".")
-    backend_module_name = ".".join(backend_path[:-1])
-    class_name = backend_path[-1]
-
-    def backend(*args, headers={}, from_address=None, **kwargs):
-        def _backend():
-            backend_module = importlib.import_module(backend_module_name)
-            backend = getattr(backend_module, class_name)
-            return backend(*args, **kwargs)
-
-        if from_address is None:
-            raise ValueError("missing from_address")
-
-        _backend.from_address = from_address
-        _backend.headers = headers
-        return _backend
-
-    return backend
-
-
-smtp_backend = _declare_backend("django.core.mail.backends.smtp.EmailBackend")
-console_backend = _declare_backend("django.core.mail.backends.console.EmailBackend")
-locmem_backend = _declare_backend("django.core.mail.backends.locmem.EmailBackend")

+ 0 - 9
tickets/mail/producers/__init__.py

@@ -1,9 +0,0 @@
-def imap_producer(**kwargs):
-    def imap_producer_factory():
-        # the import needs to be delayed until call to enable
-        # using the wrapper in the django settings
-        from .imap import imap_producer
-
-        return imap_producer(**kwargs)
-
-    return imap_producer_factory

+ 0 - 98
tickets/mail/producers/imap.py

@@ -1,98 +0,0 @@
-import email
-import email.parser
-import imaplib
-import logging
-import time
-
-from email.policy import default
-from contextlib import contextmanager
-
-logger = logging.getLogger(__name__)
-
-
-def imap_check(command_tuple):
-    status, ids = command_tuple
-    assert status == "OK", ids
-
-
-@contextmanager
-def imap_connect(host, port, username, password):
-    conn = imaplib.IMAP4_SSL(host=host, port=port)
-    conn.login(username, password)
-    imap_check(conn.list())
-    try:
-        yield conn
-    finally:
-        conn.close()
-
-
-def parse_message(message):
-    for response_part in message:
-        if not isinstance(response_part, tuple):
-            continue
-
-        message_metadata, message_content = response_part
-        email_parser = email.parser.BytesFeedParser(policy=default)
-        email_parser.feed(message_content)
-        return email_parser.close()
-
-
-def search_message(conn, *filters):
-    status, message_ids = conn.search(None, *filters)
-    for message_id in message_ids[0].split():
-        status, message = conn.fetch(message_id, "(RFC822)")
-        yield message_id, parse_message(message)
-
-
-def imap_producer(
-    process_all=False,
-    preserve=False,
-    host=None,
-    port=993,
-    username=None,
-    password=None,
-    nap_duration=1,
-    input_folder="INBOX",
-):
-    logger.debug("starting IMAP worker")
-    imap_filter = "(ALL)" if process_all else "(UNSEEN)"
-
-    def process_batch():
-        logger.debug("starting to process batch")
-        # reconnect each time to avoid repeated failures due to a lost connection
-        with imap_connect(host, port, username, password) as conn:
-            # select the requested folder
-            imap_check(conn.select(input_folder, readonly=False))
-
-            try:
-                for message_uid, message in search_message(conn, imap_filter):
-                    logger.info(f"received message {message_uid}")
-                    try:
-                        yield message
-                    except Exception:
-                        logger.exception(f"something went wrong while processing {message_uid}")
-                        raise
-
-                    if not preserve:
-                        # tag the message for deletion
-                        conn.store(message_uid, "+FLAGS", "\\Deleted")
-                else:
-                    logger.debug("did not receive any message")
-            finally:
-                if not preserve:
-                    # flush deleted messages
-                    conn.expunge()
-
-    while True:
-        try:
-            yield from process_batch()
-        except (GeneratorExit, KeyboardInterrupt):
-            # the generator was closed, due to the consumer
-            # breaking out of the loop, or an exception occuring
-            raise
-        except Exception:
-            logger.exception("mail fetching went wrong, retrying")
-
-        # sleep to avoid using too much resources
-        # tickets: get notified when a new message arrives
-        time.sleep(nap_duration)

+ 0 - 0
tickets/management/__init__.py


+ 0 - 0
tickets/management/commands/__init__.py


+ 0 - 41
tickets/management/commands/mail_worker.py

@@ -1,41 +0,0 @@
-import logging
-import socket
-import sys
-
-from django.core.management.base import BaseCommand
-from django.conf import settings
-
-logger = logging.getLogger(__name__)
-
-
-DEFAULT_IMAP_TIMEOUT = 20
-
-
-class Command(BaseCommand):
-    help = "Starts a mail worker"
-
-    def add_arguments(self, parser):
-        parser.add_argument("--imap_timeout", type=int, default=30)
-        parser.add_argument("worker_name")
-
-    def handle(self, *args, **options):
-        if not hasattr(settings, "TICKETS_MAIL_TRACKERS"):
-            logger.error("missing TICKETS_MAIL_TRACKERS setting")
-            sys.exit(1)
-
-        worker_name = options["worker_name"]
-        tracker = settings.TICKETS_MAIL_TRACKERS.get(worker_name, None)
-        if tracker is None:
-            logger.error("couldn't find configuration for %r in TICKETS_MAIL_TRACKERS", worker_name)
-            sys.exit(1)
-
-        # set the default socket timeout (imaplib doesn't enable configuring it)
-        timeout = options["imap_timeout"]
-        if timeout:
-            socket.setdefaulttimeout(timeout)
-
-        # run the mail polling loop
-        producer = tracker["producer"]
-        consumer = tracker["consumer"]
-
-        consumer(producer())

+ 0 - 261
tickets/migrations/0001_initial.py

@@ -1,261 +0,0 @@
-# Generated by Django 4.1.3 on 2023-03-31 19:43
-
-import datetime
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
-import tickets.models
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-        ("auth", "0012_alter_user_first_name_max_length"),
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name="TicketType",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "name",
-                    models.CharField(max_length=32, unique=True, verbose_name="Name"),
-                ),
-                (
-                    "life_cycle",
-                    models.CharField(
-                        max_length=192, unique=True, verbose_name="Life cycle"
-                    ),
-                ),
-            ],
-        ),
-        migrations.CreateModel(
-            name="TaskList",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                ("name", models.CharField(max_length=48, verbose_name="Name")),
-                ("slug", models.SlugField(default="", verbose_name="Slug")),
-                (
-                    "group",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to="auth.group",
-                        verbose_name="Group",
-                    ),
-                ),
-            ],
-            options={
-                "verbose_name_plural": "Ticket Lists",
-                "ordering": ["name"],
-                "unique_together": {("group", "slug")},
-            },
-        ),
-        migrations.CreateModel(
-            name="Task",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "status",
-                    models.PositiveSmallIntegerField(
-                        blank=True, null=True, verbose_name="Status"
-                    ),
-                ),
-                ("created_date", models.DateField(verbose_name="Created Date")),
-                (
-                    "status_changed_date",
-                    models.DateTimeField(
-                        blank=True, verbose_name="Status changed date"
-                    ),
-                ),
-                (
-                    "priority",
-                    models.PositiveSmallIntegerField(
-                        default=0, verbose_name="Priority"
-                    ),
-                ),
-                ("title", models.CharField(max_length=64, verbose_name="Title")),
-                ("note", models.TextField(blank=True, null=True, verbose_name="Note")),
-                (
-                    "due_date",
-                    models.DateField(blank=True, null=True, verbose_name="Due date"),
-                ),
-                (
-                    "assigned_to",
-                    models.ForeignKey(
-                        blank=True,
-                        null=True,
-                        on_delete=django.db.models.deletion.SET_NULL,
-                        related_name="tickets_assigned_to",
-                        to=settings.AUTH_USER_MODEL,
-                        verbose_name="Assigned to",
-                    ),
-                ),
-                (
-                    "created_by",
-                    models.ForeignKey(
-                        blank=True,
-                        null=True,
-                        on_delete=django.db.models.deletion.SET_NULL,
-                        to=settings.AUTH_USER_MODEL,
-                        verbose_name="Created by",
-                    ),
-                ),
-                (
-                    "task_list",
-                    models.ForeignKey(
-                        blank=True,
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to="tickets.tasklist",
-                        verbose_name="Task list",
-                    ),
-                ),
-                (
-                    "type",
-                    models.ForeignKey(
-                        default=0,
-                        null=True,
-                        on_delete=django.db.models.deletion.SET_NULL,
-                        related_name="tickets_tickettype",
-                        to="tickets.tickettype",
-                        verbose_name="Type",
-                    ),
-                ),
-            ],
-            options={
-                "verbose_name": "Task",
-                "ordering": ["priority", "created_date"],
-            },
-        ),
-        migrations.CreateModel(
-            name="Attachment",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "timestamp",
-                    models.DateTimeField(
-                        default=datetime.datetime.now, verbose_name="Timestamp"
-                    ),
-                ),
-                (
-                    "file",
-                    models.FileField(
-                        max_length=255,
-                        upload_to=tickets.models.get_attachment_upload_dir,
-                        verbose_name="File",
-                    ),
-                ),
-                (
-                    "added_by",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to=settings.AUTH_USER_MODEL,
-                        verbose_name="Added by",
-                    ),
-                ),
-                (
-                    "task",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to="tickets.task",
-                        verbose_name="Task",
-                    ),
-                ),
-            ],
-        ),
-        migrations.CreateModel(
-            name="Comment",
-            fields=[
-                (
-                    "id",
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                (
-                    "date",
-                    models.DateTimeField(
-                        default=django.utils.timezone.now, verbose_name="Date"
-                    ),
-                ),
-                (
-                    "email_from",
-                    models.CharField(
-                        blank=True, max_length=320, null=True, verbose_name="Eamil from"
-                    ),
-                ),
-                (
-                    "email_message_id",
-                    models.CharField(
-                        blank=True,
-                        max_length=255,
-                        null=True,
-                        verbose_name="Eamil message id",
-                    ),
-                ),
-                ("body", models.TextField(blank=True, verbose_name="Body")),
-                (
-                    "author",
-                    models.ForeignKey(
-                        blank=True,
-                        null=True,
-                        on_delete=django.db.models.deletion.CASCADE,
-                        related_name="tickets_comments",
-                        to=settings.AUTH_USER_MODEL,
-                        verbose_name="Author",
-                    ),
-                ),
-                (
-                    "task",
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        to="tickets.task",
-                        verbose_name="Task",
-                    ),
-                ),
-            ],
-            options={
-                "verbose_name": "Comment",
-                "unique_together": {("task", "email_message_id")},
-            },
-        ),
-    ]

+ 0 - 0
tickets/migrations/__init__.py


+ 0 - 221
tickets/models.py

@@ -1,221 +0,0 @@
-import datetime
-import os
-import textwrap
-from django.utils import timezone
-
-from django.conf import settings
-from django.contrib.auth.models import Group
-from django.db import DEFAULT_DB_ALIAS, models
-from django.db.transaction import Atomic, get_connection
-from django.urls import reverse
-
-def get_attachment_upload_dir(instance, filename):
-    """Determine upload dir for task attachment files.
-    """
-
-    return "/".join(["tasks", "attachments", str(instance.task.id), filename])
-
-
-class LockedAtomicTransaction(Atomic):
-    """
-    modified from https://stackoverflow.com/a/41831049
-    this is needed for safely merging
-
-    Does a atomic transaction, but also locks the entire table for any transactions, for the duration of this
-    transaction. Although this is the only way to avoid concurrency issues in certain situations, it should be used with
-    caution, since it has impacts on performance, for obvious reasons...
-    """
-
-    def __init__(self, *models, using=None, savepoint=None):
-        if using is None:
-            using = DEFAULT_DB_ALIAS
-        super().__init__(using, savepoint)
-        self.models = models
-
-    def __enter__(self):
-        super(LockedAtomicTransaction, self).__enter__()
-
-        # Make sure not to lock, when sqlite is used, or you'll run into problems while running tests!!!
-        if settings.DATABASES[self.using]["ENGINE"] != "django.db.backends.sqlite3":
-            cursor = None
-            try:
-                cursor = get_connection(self.using).cursor()
-                for model in self.models:
-                    cursor.execute(
-                        "LOCK TABLE {table_name}".format(table_name=model._meta.db_table)
-                    )
-            finally:
-                if cursor and not cursor.closed:
-                    cursor.close()
-
-
-class TaskList(models.Model):
-    name = models.CharField(max_length=48, verbose_name='Name')
-    slug = models.SlugField(default="", verbose_name='Slug')
-    group = models.ForeignKey(Group, on_delete=models.CASCADE, verbose_name='Group')
-
-    def __str__(self):
-        return self.name
-
-    class Meta:
-        ordering = ["name"]
-        verbose_name_plural = "Ticket Lists"
-        unique_together = ("group", "slug")
-
-
-class TicketType(models.Model):
-    name = models.CharField(max_length=32, unique=True, editable=True, verbose_name='Name')
-    life_cycle = models.CharField(max_length=192, unique=True, editable=True, verbose_name='Life cycle')
-
-    def __str__(self):
-        return self.name
-
-
-class Task(models.Model):
-    task_list = models.ForeignKey(TaskList, blank=True, on_delete=models.CASCADE, verbose_name="Task list")
-    status = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name="Status")
-    created_date = models.DateField(verbose_name="Created Date")
-    status_changed_date = models.DateTimeField(blank=True, verbose_name="Status changed date")
-    created_by = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        blank=True,
-        null=True,
-        on_delete=models.SET_NULL,
-        verbose_name="Created by"
-    )
-    priority = models.PositiveSmallIntegerField(default=0, verbose_name="Priority")
-    type = models.ForeignKey(
-        TicketType,
-        on_delete=models.SET_NULL,
-        default=0,
-        null=True,
-        related_name="tickets_tickettype",
-        verbose_name="Type")
-    title = models.CharField(max_length=64, verbose_name="Title")
-    note = models.TextField(blank=True, null=True, verbose_name="Note")
-    due_date = models.DateField(blank=True, null=True, verbose_name="Due date")
-    assigned_to = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        blank=True,
-        null=True,
-        related_name="tickets_assigned_to",
-        on_delete=models.SET_NULL,
-        verbose_name="Assigned to"
-    )
-
-    def get_states_list(self):
-        data = self.type.life_cycle
-        data = data.split(",")
-        for i, item in enumerate(data):
-            data[i] = item.split("-")
-            for inner_index, inner_item in enumerate(data[i]):
-                data[i][inner_index] = int(inner_item)
-        return data
-
-    def now_state_list(self):
-        states_list = self.get_states_list()
-        if self.status == None:
-            return states_list[0]
-        else:
-            for i in range(len(states_list)):
-                if states_list[i][0] == self.status:
-                    return states_list[i]
-
-    def next_state(self):
-        states_list = self.get_states_list()
-        try:
-            return self.now_state_list(self, states_list)[1]
-        except IndexError:
-            return 1
-    
-    def first_state(self):
-        return self.get_states_list()[0][0]
-
-    # Has due date for an instance of this object passed?
-    def overdue_status(self):
-        "Returns whether the Tasks's due date has passed or not."
-        if self.due_date and datetime.date.today() > self.due_date:
-            return True
-
-    def __str__(self):
-        return self.title
-
-    def get_absolute_url(self):
-        return reverse("tickets:task_detail", kwargs={"task_id": self.id})
-
-    def save(self, **kwargs):
-        if not self.id:
-            self.created_date = timezone.now()
-        self.status_changed_date = timezone.now()
-        super(Task, self).save()
-    
-    # def merge_into(self, merge_target):
-    #     if merge_target.pk == self.pk:
-    #         raise ValueError("can't merge a task with self")
-
-    #     # lock the comments to avoid concurrent additions of comments after the
-    #     # update request. these comments would be irremediably lost because of
-    #     # the cascade clause
-    #     with LockedAtomicTransaction(Comment):
-    #         Comment.objects.filter(task=self).update(task=merge_target)
-    #         self.delete()
-    class Meta:
-        verbose_name = "Task"
-        ordering = ["priority", "created_date"]
-
-
-class Comment(models.Model):
-    author = models.ForeignKey(
-        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True,
-        related_name="tickets_comments",
-        verbose_name='Author'
-    )
-    task = models.ForeignKey(Task, on_delete=models.CASCADE, verbose_name='Task')
-    date = models.DateTimeField(default=timezone.now, verbose_name='Date')
-    email_from = models.CharField(max_length=320, blank=True, null=True, verbose_name='Eamil from')
-    email_message_id = models.CharField(max_length=255, blank=True, null=True, verbose_name='Eamil message id')
-
-    body = models.TextField(blank=True, verbose_name='Body')
-
-    class Meta:
-        # an email should only appear once per task
-        verbose_name='Comment'
-        unique_together = ("task", "email_message_id")
-
-    @property
-    def author_text(self):
-        if self.author is not None:
-            return str(self.author)
-
-        assert self.email_message_id is not None
-        return str(self.email_from)
-
-    @property
-    def snippet(self):
-        body_snippet = textwrap.shorten(self.body, width=35, placeholder="...")
-        # Define here rather than in __str__ so we can use it in the admin list_display
-        return "{author} - {snippet}...".format(author=self.author_text, snippet=body_snippet)
-
-    def __str__(self):
-        return self.snippet
-
-
-class Attachment(models.Model):
-    """
-    Defines a generic file attachment for use in M2M relation with Task.
-    """
-
-    task = models.ForeignKey(Task, on_delete=models.CASCADE, verbose_name='Task')
-    added_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='Added by')
-    timestamp = models.DateTimeField(default=datetime.datetime.now, verbose_name='Timestamp')
-    file = models.FileField(upload_to=get_attachment_upload_dir, max_length=255, verbose_name='File')
-
-    def filename(self):
-        return os.path.basename(self.file.name)
-
-    def extension(self):
-        name, extension = os.path.splitext(self.file.name)
-        return extension
-
-    def __str__(self):
-        return f"{self.task.id} - {self.file.name}"

+ 0 - 0
tickets/operations/__init__.py


+ 0 - 201
tickets/operations/csv_importer.py

@@ -1,201 +0,0 @@
-import codecs
-import csv
-import datetime
-import logging
-
-from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
-
-from tickets.models import Task, TaskList
-
-log = logging.getLogger(__name__)
-
-
-class CSVImporter:
-    """Core upsert functionality for CSV import, for re-use by `import_csv` management command, web UI and tests.
-    Supplies a detailed log of what was and was not imported at the end. See README for usage notes.
-    """
-
-    def __init__(self):
-        self.errors = []
-        self.upserts = []
-        self.summaries = []
-        self.line_count = 0
-        self.upsert_count = 0
-
-    def upsert(self, fileobj, as_string_obj=False):
-        """Expects a file *object*, not a file path. This is important because this has to work for both
-        the management command and the web uploader; the web uploader will pass in in-memory file
-        with no path!
-
-        Header row is:
-        Title, Group, Task List, Created Date, Due Date, Completed, Created By, Assigned To, Note, Priority
-        """
-
-        if as_string_obj:
-            # fileobj comes from mgmt command
-            csv_reader = csv.DictReader(fileobj)
-        else:
-            # fileobj comes from browser upload (in-memory)
-            csv_reader = csv.DictReader(codecs.iterdecode(fileobj, "utf-8"))
-
-        # DI check: Do we have expected header row?
-        header = csv_reader.fieldnames
-        expected = [
-            "Title",
-            "Group",
-            "Task List",
-            "Created By",
-            "Created Date",
-            "Due Date",
-            "Completed",
-            "Assigned To",
-            "Note",
-            "Priority",
-        ]
-        if header != expected:
-            self.errors.append(
-                f"Inbound data does not have expected columns.\nShould be: {expected}"
-            )
-            return
-
-        for row in csv_reader:
-            self.line_count += 1
-
-            newrow = self.validate_row(row)
-            if newrow:
-                # newrow at this point is fully validated, and all FK relations exist,
-                # e.g. `newrow.get("Assigned To")`, is a Django User instance.
-                assignee = newrow.get("Assigned To") if newrow.get("Assigned To") else None
-                created_date = (
-                    newrow.get("Created Date")
-                    if newrow.get("Created Date")
-                    else datetime.datetime.today()
-                )
-                due_date = newrow.get("Due Date") if newrow.get("Due Date") else None
-                priority = newrow.get("Priority") if newrow.get("Priority") else None
-
-                obj, created = Task.objects.update_or_create(
-                    created_by=newrow.get("Created By"),
-                    task_list=newrow.get("Task List"),
-                    title=newrow.get("Title"),
-                    defaults={
-                        "assigned_to": assignee,
-                        "completed": newrow.get("Completed"),
-                        "created_date": created_date,
-                        "due_date": due_date,
-                        "note": newrow.get("Note"),
-                        "priority": priority,
-                    },
-                )
-                self.upsert_count += 1
-                msg = (
-                    f'Upserted task {obj.id}: "{obj.title}"'
-                    f' in list "{obj.task_list}" (group "{obj.task_list.group}")'
-                )
-                self.upserts.append(msg)
-
-        self.summaries.append(f"Processed {self.line_count} CSV rows")
-        self.summaries.append(f"Upserted {self.upsert_count} rows")
-        self.summaries.append(f"Skipped {self.line_count - self.upsert_count} rows")
-
-        return {"summaries": self.summaries, "upserts": self.upserts, "errors": self.errors}
-
-    def validate_row(self, row):
-        """Perform data integrity checks and set default values. Returns a valid object for insertion, or False.
-        Errors are stored for later display. Intentionally not broken up into separate validator functions because
-        there are interdpendencies, such as checking for existing `creator` in one place and then using
-        that creator for group membership check in others."""
-
-        row_errors = []
-
-        # #######################
-        # Task creator must exist
-        if not row.get("Created By"):
-            msg = f"Missing required task creator."
-            row_errors.append(msg)
-
-        creator = get_user_model().objects.filter(username=row.get("Created By")).first()
-        if not creator:
-            msg = f"Invalid task creator {row.get('Created By')}"
-            row_errors.append(msg)
-
-        # #######################
-        # If specified, Assignee must exist
-        assignee = None  # Perfectly valid
-        if row.get("Assigned To"):
-            assigned = get_user_model().objects.filter(username=row.get("Assigned To"))
-            if assigned.exists():
-                assignee = assigned.first()
-            else:
-                msg = f"Missing or invalid task assignee {row.get('Assigned To')}"
-                row_errors.append(msg)
-
-        # #######################
-        # Group must exist
-        try:
-            target_group = Group.objects.get(name=row.get("Group"))
-        except Group.DoesNotExist:
-            msg = f"Could not find group {row.get('Group')}."
-            row_errors.append(msg)
-            target_group = None
-
-        # #######################
-        # Task creator must be in the target group
-        if creator and target_group not in creator.groups.all():
-            msg = f"{creator} is not in group {target_group}"
-            row_errors.append(msg)
-
-        # #######################
-        # Assignee must be in the target group
-        if assignee and target_group not in assignee.groups.all():
-            msg = f"{assignee} is not in group {target_group}"
-            row_errors.append(msg)
-
-        # #######################
-        # Task list must exist in the target group
-        try:
-            tasklist = TaskList.objects.get(name=row.get("Task List"), group=target_group)
-            row["Task List"] = tasklist
-        except TaskList.DoesNotExist:
-            msg = f"Task list {row.get('Task List')} in group {target_group} does not exist"
-            row_errors.append(msg)
-
-        # #######################
-        # Validate Dates
-        datefields = ["Due Date", "Created Date"]
-        for datefield in datefields:
-            datestring = row.get(datefield)
-            if datestring:
-                valid_date = self.validate_date(datestring)
-                if valid_date:
-                    row[datefield] = valid_date
-                else:
-                    msg = f"Could not convert {datefield} {datestring} to valid date instance"
-                    row_errors.append(msg)
-
-        # #######################
-        # Group membership checks have passed
-        row["Created By"] = creator
-        row["Group"] = target_group
-        if assignee:
-            row["Assigned To"] = assignee
-
-        # Set Completed
-        row["Completed"] = row["Completed"] == "Yes"
-
-        # #######################
-        if row_errors:
-            self.errors.append({self.line_count: row_errors})
-            return False
-
-        # No errors:
-        return row
-
-    def validate_date(self, datestring):
-        """Inbound date string from CSV translates to a valid python date."""
-        try:
-            date_obj = datetime.datetime.strptime(datestring, "%Y-%m-%d")
-            return date_obj
-        except ValueError:
-            return False

+ 0 - 32
tickets/queries.py

@@ -1,32 +0,0 @@
-from django.db import connection
-
-class Query:
-    #@staticmethod
-    def sql_query(method):
-        def wrapper():
-            with connection.cursor() as cursor:
-                method(cursor)
-        return wrapper
-
-
-    @sql_query
-    #@staticmethod
-    def get_tickets(cursor):
-        cursor.execute("""
-        SELECT
-        tickets_task.id,
-        tickets_task.title,
-        SharixAdmin_sharixuser.username,
-        tickets_task.created_by_id,
-        tickets_task.priority
-        FROM
-        tickets_task
-        JOIN
-        SharixAdmin_sharixuser
-        ON
-        tickets_task.created_by_id == SharixAdmin_sharixuser.id
-        ORDER BY
-        tickets_task.priority;
-        """)
-        rows = cursor.fetchall()
-        print(rows)

+ 0 - 19
tickets/requirements.txt

@@ -1,19 +0,0 @@
-asgiref
-bleach
-Django
-django-autocomplete-light
-django-extensions
-factory-boy
-Faker
-flake8
-html2text
-mccabe
-psycopg2-binary
-pycodestyle
-pyflakes
-python-dateutil
-six
-sqlparse
-titlecase
-tzdata
-webencodings

+ 0 - 27
tickets/serializer.py

@@ -1,27 +0,0 @@
-from rest_framework import serializers
-from tickets.models import *
-
-class TaskSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Task
-        fields = "__all__"
-
-class TaskListSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = TaskList
-        fields = "__all__"
-
-class TicketTypeSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = TicketType
-        fields = "__all__"
-
-class CommentSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Comment
-        fields = "__all__"
-
-class AttachmentSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Attachment
-        fields = "__all__"

+ 0 - 382
tickets/static/tickets/js/jquery.tablednd_0_5.js

@@ -1,382 +0,0 @@
-/**
- * TableDnD plug-in for JQuery, allows you to drag and drop table rows
- * You can set up various options to control how the system will work
- * Copyright (c) Denis Howlett <denish@isocra.com>
- * Licensed like jQuery, see http://docs.jquery.com/License.
- *
- * Configuration options:
- * 
- * onDragStyle
- *     This is the style that is assigned to the row during drag. There are limitations to the styles that can be
- *     associated with a row (such as you can't assign a border--well you can, but it won't be
- *     displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
- *     a map (as used in the jQuery css(...) function).
- * onDropStyle
- *     This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
- *     to what you can do. Also this replaces the original style, so again consider using onDragClass which
- *     is simply added and then removed on drop.
- * onDragClass
- *     This class is added for the duration of the drag and then removed when the row is dropped. It is more
- *     flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
- *     is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
- *     stylesheet.
- * onDrop
- *     Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
- *     and the row that was dropped. You can work out the new order of the rows by using
- *     table.rows.
- * onDragStart
- *     Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
- *     table and the row which the user has started to drag.
- * onAllowDrop
- *     Pass a function that will be called as a row is over another row. If the function returns true, allow 
- *     dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
- *     the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
- * scrollAmount
- *     This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
- *     window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
- *     FF3 beta
- * dragHandle
- *     This is the name of a class that you assign to one or more cells in each row that is draggable. If you
- *     specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
- *     will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
- *     the whole row is draggable.
- * 
- * Other ways to control behaviour:
- *
- * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
- * that you don't want to be draggable.
- *
- * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
- * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
- * an ID as must all the rows.
- *
- * Other methods:
- *
- * $("...").tableDnDUpdate() 
- * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
- * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
- * The table maintains the original configuration (so you don't have to specify it again).
- *
- * $("...").tableDnDSerialize()
- * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
- * called from anywhere and isn't dependent on the currentTable being set up correctly before calling
- *
- * Known problems:
- * - Auto-scoll has some problems with IE7  (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
- * 
- * Version 0.2: 2008-02-20 First public version
- * Version 0.3: 2008-02-07 Added onDragStart option
- *                         Made the scroll amount configurable (default is 5 as before)
- * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
- *                         Added onAllowDrop to control dropping
- *                         Fixed a bug which meant that you couldn't set the scroll amount in both directions
- *                         Added serialize method
- * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
- *                         draggable
- *                         Improved the serialize method to use a default (and settable) regular expression.
- *                         Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
- */
-jQuery.tableDnD = {
-    /** Keep hold of the current table being dragged */
-    currentTable : null,
-    /** Keep hold of the current drag object if any */
-    dragObject: null,
-    /** The current mouse offset */
-    mouseOffset: null,
-    /** Remember the old value of Y so that we don't do too much processing */
-    oldY: 0,
-
-    /** Actually build the structure */
-    build: function(options) {
-        // Set up the defaults if any
-
-        this.each(function() {
-            // This is bound to each matching table, set up the defaults and override with user options
-            this.tableDnDConfig = jQuery.extend({
-                onDragStyle: null,
-                onDropStyle: null,
-				// Add in the default class for whileDragging
-				onDragClass: "tDnD_whileDrag",
-                onDrop: null,
-                onDragStart: null,
-                scrollAmount: 5,
-				serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
-				serializeParamName: null, // If you want to specify another parameter name instead of the table ID
-                dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
-            }, options || {});
-            // Now make the rows draggable
-            jQuery.tableDnD.makeDraggable(this);
-        });
-
-        // Now we need to capture the mouse up and mouse move event
-        // We can use bind so that we don't interfere with other event handlers
-        jQuery(document)
-            .bind('mousemove', jQuery.tableDnD.mousemove)
-            .bind('mouseup', jQuery.tableDnD.mouseup);
-
-        // Don't break the chain
-        return this;
-    },
-
-    /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
-    makeDraggable: function(table) {
-        var config = table.tableDnDConfig;
-		if (table.tableDnDConfig.dragHandle) {
-			// We only need to add the event to the specified cells
-			var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
-			cells.each(function() {
-				// The cell is bound to "this"
-                jQuery(this).mousedown(function(ev) {
-                    jQuery.tableDnD.dragObject = this.parentNode;
-                    jQuery.tableDnD.currentTable = table;
-                    jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
-                    if (config.onDragStart) {
-                        // Call the onDrop method if there is one
-                        config.onDragStart(table, this);
-                    }
-                    return false;
-                });
-			})
-		} else {
-			// For backwards compatibility, we add the event to the whole row
-	        var rows = jQuery("tr", table); // get all the rows as a wrapped set
-	        rows.each(function() {
-				// Iterate through each row, the row is bound to "this"
-				var row = jQuery(this);
-				if (! row.hasClass("nodrag")) {
-	                row.mousedown(function(ev) {
-	                    if (ev.target.tagName == "TD") {
-	                        jQuery.tableDnD.dragObject = this;
-	                        jQuery.tableDnD.currentTable = table;
-	                        jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
-	                        if (config.onDragStart) {
-	                            // Call the onDrop method if there is one
-	                            config.onDragStart(table, this);
-	                        }
-	                        return false;
-	                    }
-	                }).css("cursor", "move"); // Store the tableDnD object
-				}
-			});
-		}
-	},
-
-	updateTables: function() {
-		this.each(function() {
-			// this is now bound to each matching table
-			if (this.tableDnDConfig) {
-				jQuery.tableDnD.makeDraggable(this);
-			}
-		})
-	},
-
-    /** Get the mouse coordinates from the event (allowing for browser differences) */
-    mouseCoords: function(ev){
-        if(ev.pageX || ev.pageY){
-            return {x:ev.pageX, y:ev.pageY};
-        }
-        return {
-            x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
-            y:ev.clientY + document.body.scrollTop  - document.body.clientTop
-        };
-    },
-
-    /** Given a target element and a mouse event, get the mouse offset from that element.
-        To do this we need the element's position and the mouse position */
-    getMouseOffset: function(target, ev) {
-        ev = ev || window.event;
-
-        var docPos    = this.getPosition(target);
-        var mousePos  = this.mouseCoords(ev);
-        return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
-    },
-
-    /** Get the position of an element by going up the DOM tree and adding up all the offsets */
-    getPosition: function(e){
-        var left = 0;
-        var top  = 0;
-        /** Safari fix -- thanks to Luis Chato for this! */
-        if (e.offsetHeight == 0) {
-            /** Safari 2 doesn't correctly grab the offsetTop of a table row
-            this is detailed here:
-            http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
-            the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
-            note that firefox will return a text node as a first child, so designing a more thorough
-            solution may need to take that into account, for now this seems to work in firefox, safari, ie */
-            e = e.firstChild; // a table cell
-        }
-
-        while (e.offsetParent){
-            left += e.offsetLeft;
-            top  += e.offsetTop;
-            e     = e.offsetParent;
-        }
-
-        left += e.offsetLeft;
-        top  += e.offsetTop;
-
-        return {x:left, y:top};
-    },
-
-    mousemove: function(ev) {
-        if (jQuery.tableDnD.dragObject == null) {
-            return;
-        }
-
-        var dragObj = jQuery(jQuery.tableDnD.dragObject);
-        var config = jQuery.tableDnD.currentTable.tableDnDConfig;
-        var mousePos = jQuery.tableDnD.mouseCoords(ev);
-        var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
-        //auto scroll the window
-	    var yOffset = window.pageYOffset;
-	 	if (document.all) {
-	        // Windows version
-	        //yOffset=document.body.scrollTop;
-	        if (typeof document.compatMode != 'undefined' &&
-	             document.compatMode != 'BackCompat') {
-	           yOffset = document.documentElement.scrollTop;
-	        }
-	        else if (typeof document.body != 'undefined') {
-	           yOffset=document.body.scrollTop;
-	        }
-
-	    }
-		    
-		if (mousePos.y-yOffset < config.scrollAmount) {
-	    	window.scrollBy(0, -config.scrollAmount);
-	    } else {
-            var windowHeight = window.innerHeight ? window.innerHeight
-                    : document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
-            if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
-                window.scrollBy(0, config.scrollAmount);
-            }
-        }
-
-
-        if (y != jQuery.tableDnD.oldY) {
-            // work out if we're going up or down...
-            var movingDown = y > jQuery.tableDnD.oldY;
-            // update the old value
-            jQuery.tableDnD.oldY = y;
-            // update the style to show we're dragging
-			if (config.onDragClass) {
-				dragObj.addClass(config.onDragClass);
-			} else {
-	            dragObj.css(config.onDragStyle);
-			}
-            // If we're over a row then move the dragged row to there so that the user sees the
-            // effect dynamically
-            var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
-            if (currentRow) {
-                // tickets worry about what happens when there are multiple TBODIES
-                if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
-                    jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
-                } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
-                    jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
-                }
-            }
-        }
-
-        return false;
-    },
-
-    /** We're only worried about the y position really, because we can only move rows up and down */
-    findDropTargetRow: function(draggedRow, y) {
-        var rows = jQuery.tableDnD.currentTable.rows;
-        for (var i=0; i<rows.length; i++) {
-            var row = rows[i];
-            var rowY    = this.getPosition(row).y;
-            var rowHeight = parseInt(row.offsetHeight)/2;
-            if (row.offsetHeight == 0) {
-                rowY = this.getPosition(row.firstChild).y;
-                rowHeight = parseInt(row.firstChild.offsetHeight)/2;
-            }
-            // Because we always have to insert before, we need to offset the height a bit
-            if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
-                // that's the row we're over
-				// If it's the same as the current row, ignore it
-				if (row == draggedRow) {return null;}
-                var config = jQuery.tableDnD.currentTable.tableDnDConfig;
-                if (config.onAllowDrop) {
-                    if (config.onAllowDrop(draggedRow, row)) {
-                        return row;
-                    } else {
-                        return null;
-                    }
-                } else {
-					// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
-                    var nodrop = jQuery(row).hasClass("nodrop");
-                    if (! nodrop) {
-                        return row;
-                    } else {
-                        return null;
-                    }
-                }
-                return row;
-            }
-        }
-        return null;
-    },
-
-    mouseup: function(e) {
-        if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
-            var droppedRow = jQuery.tableDnD.dragObject;
-            var config = jQuery.tableDnD.currentTable.tableDnDConfig;
-            // If we have a dragObject, then we need to release it,
-            // The row will already have been moved to the right place so we just reset stuff
-			if (config.onDragClass) {
-	            jQuery(droppedRow).removeClass(config.onDragClass);
-			} else {
-	            jQuery(droppedRow).css(config.onDropStyle);
-			}
-            jQuery.tableDnD.dragObject   = null;
-            if (config.onDrop) {
-                // Call the onDrop method if there is one
-                config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
-            }
-            jQuery.tableDnD.currentTable = null; // let go of the table too
-        }
-    },
-
-    serialize: function() {
-        if (jQuery.tableDnD.currentTable) {
-            return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
-        } else {
-            return "Error: No Table id set, you need to set an id on your table and every row";
-        }
-    },
-
-	serializeTable: function(table) {
-        var result = "";
-        var tableId = table.id;
-        var rows = table.rows;
-        for (var i=0; i<rows.length; i++) {
-            if (result.length > 0) result += "&";
-            var rowId = rows[i].id;
-            if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
-                rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
-            }
-
-            result += tableId + '[]=' + rowId;
-        }
-        return result;
-	},
-
-	serializeTables: function() {
-        var result = "";
-        this.each(function() {
-			// this is now bound to each matching table
-			result += jQuery.tableDnD.serializeTable(this);
-		});
-        return result;
-    }
-
-}
-
-jQuery.fn.extend(
-	{
-		tableDnD : jQuery.tableDnD.build,
-		tableDnDUpdate : jQuery.tableDnD.updateTables,
-		tableDnDSerialize: jQuery.tableDnD.serializeTables
-	}
-);

+ 0 - 5
tickets/template_tags/custom_tags.py

@@ -1,5 +0,0 @@
-from django.template.defaulttags import register
-
-@register.filter
-def get_value(dictionary, key):
-    return dictionary.get(key)

+ 0 - 83
tickets/templates/tickets/base.html

@@ -1,83 +0,0 @@
-{% load static %}
-
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <meta name="description" content="">
-    <meta name="author" content="">
-    <link rel="icon" href="/static/favicon.ico">
-
-    <title>Мои заявки</title>
-
-    <!-- Bootstrap core CSS -->
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
-
-    <!-- Custom styles for this template -->
-    <link href="/static/css/sticky-footer-navbar.css" rel="stylesheet">
-  </head>
-
-  <body>
-
-    <header>
-      <!-- Fixed navbar -->
-      <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
-        <a class="navbar-brand" href="/">Мои заявки</a>
-        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
-          <span class="navbar-toggler-icon"></span>
-        </button>
-        <div class="collapse navbar-collapse" id="navbarCollapse">
-          <ul class="navbar-nav mr-auto">
-            {% if user.is_authenticated %}
-              <li class="nav-item"><a href="{% url 'tickets:mine' %}" class="nav-link">My Tickets</a></li>
-               <li class="nav-item"><a href="{% url 'tickets:lists' %}" class="nav-link">All tickets</a></li> 
-            {% endif %}
-          </ul>
-
-          {% if user.is_authenticated %}
-      		  <form class="form-inline mt-2 mt-md-0" action="{% url 'tickets:search' %}" method="get" placeholder="Search" aria-label="Search">
-              <input type="text" name="q" value="" class="form-control mr-sm-2" id="q">
-              <input type="submit" value="Search Tasks" class="btn btn-outline-success my-2 my-sm-0">
-            </form>
-          {% endif %}
-        </div>
-      </nav>
-    </header>
-
-    <!-- Begin page content -->
-    <main role="main" class="container" style="padding-top: 70px;">
-      {% if messages %}
-        {% for message in messages %}
-          <div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">{{ message }}</div>
-        {% endfor %}
-      {% endif %}
-
-      <p>{{ form.non_field_errors }}</p>
-
-      {% block content %}{% endblock %}
-    </main>
-
-    <footer class="footer">
-      <div class="container">
-        <span class="text-muted">
-          django-tickets, {% now "Y" %}.
-          {% if user.is_authenticated %}Logged in as {{ user.username }}{% endif %}</span>
-      </div>
-    </footer>
-
-    <script>
-      function handleClick(el, inactive, active) {
-        el.innerHTML = el.innerHTML == inactive ? active : inactive;
-      }
-    </script>
-
-    <!-- jQuery and Bootstrap core JS, plus Popper -->
-    <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
-    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
-
-    {% block extra_js %}{% endblock extra_js %}
-
-  </body>
-</html>

+ 0 - 19
tickets/templates/tickets/create_list.html

@@ -1,19 +0,0 @@
-{% extends "tickets/base.html" %}
-
-
-{% block content %}
-  <h2>Create new list:</h2>
-
-  <form method="post">
-    {% csrf_token %}
-    <div class="form-group">
-      <label for="id_name">Name</label>
-      {{ form.name }}
-    </div>
-    <div class="form-group">
-      <label for="id_group">Group</label>
-      {{ form.group }}
-    </div>
-    <button type="submit" class="btn btn-primary">Create</button>
-  </form>
-{% endblock %}

+ 0 - 20
tickets/templates/tickets/del_list.html

@@ -1,20 +0,0 @@
-{% extends "tickets/base.html" %}
-{% block title %}Delete list{% endblock %}
-
-{% block content %}
-  {% if user.is_staff %}
-  <h1>Delete entire list: {{ task_list.name }} ?</h1>
-
-  <form action="" method="post" accept-charset="utf-8">
-    {% csrf_token %}
-    <input type="hidden" name="task_list" value="{{ task_list.id }}">
-    <p>
-      <a href="{% url 'tickets:list_detail' task_list.id task_list.slug %}" class="btn btn-success">Return to list: {{ task_list.name }}</a>
-      <input type="submit" name="delete-confirm" value="Do it! &rarr;" class="btn btn-danger">
-    </p>
-  </form>
-
-  {% else %}
-    <p>Sorry, you don't have permission to delete lists. Please contact your group administrator.</p>
-  {% endif %}
-{% endblock %}

+ 0 - 17
tickets/templates/tickets/email/assigned_body.txt

@@ -1,17 +0,0 @@
-{{ task.assigned_to.first_name }} - {{ task.assigned_to.last_name }}
-
-A new task on the list {{ task.task_list.name }} has been assigned to you by {{ task.created_by.get_full_name }}:
-
-{{ task.title }}
-
-{% if task.note %}
-{% autoescape off %}
-Note: {{ task.note }}
-{% endautoescape %}
-{% endif %}
-
-Task details/comments:
-http://{{ site }}{% url 'tickets:task_detail' task.id %}
-
-List {{ task.task_list.name }}:
-http://{{ site }}{% url 'tickets:list_detail' task.task_list.id task.task_list.slug %}

+ 0 - 1
tickets/templates/tickets/email/assigned_subject.txt

@@ -1 +0,0 @@
-A new task has been assigned to you - {% autoescape off %}{{ task.title }}{% endautoescape %}

+ 0 - 17
tickets/templates/tickets/email/changetask_body.txt

@@ -1,17 +0,0 @@
-{{ task.assigned_to.first_name }} - {{ task.assigned_to.last_name }}
-
-A task on the list {{ task.task_list.name }} has been changed to you by {{ task.created_by.get_full_name }}:
-
-{{ task.title }}
-
-{% if task.note %}
-{% autoescape off %}
-Note: {{ task.note }}
-{% endautoescape %}
-{% endif %}
-
-Task details/comments:
-http://{{ site }}{% url 'tickets:task_detail' task.id %}
-
-List {{ task.task_list.name }}:
-http://{{ site }}{% url 'tickets:list_detail' task.task_list.id task.task_list.slug %}

+ 0 - 16
tickets/templates/tickets/email/newcomment_body.txt

@@ -1,16 +0,0 @@
-A new task comment has been added.
-
-Task: {{ task.title }}
-Commenter: {{ user.first_name }} {{ user.last_name }}
-
-Comment:
-{% autoescape off %}
-{{ body }}
-{% endautoescape %}
-
-Task details/comments:
-https://{{ site }}{% url 'tickets:task_detail' task.id %}
-
-List {{ task.task_list.name }}:
-https://{{ site }}{% url 'tickets:list_detail' task.task_list.id task.task_list.slug %}
-

+ 0 - 84
tickets/templates/tickets/import_csv.html

@@ -1,84 +0,0 @@
-{% extends "tickets/base.html" %}
-{% load static %}
-
-{% block title %}Import CSV{% endblock %}
-
-{% block content %}
-  <h2>
-    Import CSV
-  </h2>
-
-  <p>
-    Batch-import tasks by uploading a specifically-formatted CSV.
-    See documentation for formatting rules.
-    Successs and failures will be reported here.
-  </p>
-
-  {% if results %}
-    <div class="card mb-4">
-      <div class="card-header">
-        Results of CSV upload
-      </div>
-      <div class="card-body">
-
-        {% if results.summaries %}
-          <p>
-            <b>Summary:</b>
-          </p>
-          <ul>
-            {% for line in results.summaries %}
-              <li>{{ line }}</li>
-            {% endfor %}
-          </ul>
-        {% endif %}
-
-        {% if results.upserts %}
-          <p>
-            <b>Upserts (tasks created or updated):</b>
-          </p>
-          <ul>
-            {% for line in results.upserts %}
-              <li>{{ line }}</li>
-            {% endfor %}
-          </ul>
-        {% endif %}
-
-        {% if results.errors %}
-          <p>
-            <b>Errors (tasks NOT created or updated):</b>
-          </p>
-          <ul>
-            {% for error_row in results.errors %}
-              {% for k, error_list in error_row.items  %}
-                <li>CSV row {{ k }}</li>
-                <ul>
-                  {% for err in error_list %}
-                    <li>{{ err }}</li>
-                  {% endfor %}
-                </ul>
-              {% endfor %}
-            {% endfor %}
-          </ul>
-        {% endif %}
-
-
-      </div>
-    </div>
-  {% endif %}
-
-  <div class="card">
-    <div class="card-header">
-      Upload Tasks
-    </div>
-    <div class="card-body">
-      <form method="post" enctype="multipart/form-data">
-        {% csrf_token %}
-        <div>
-          <input type="file" name="csvfile" accept="text/csv">
-        </div>
-        <button type="submit" class="btn btn-primary mt-4">Upload</button>
-      </form>
-    </div>
-  </div>
-
-{% endblock %}

+ 0 - 1
tickets/templates/tickets/include/delete_list.html

@@ -1 +0,0 @@
-<a href="{% url 'tickets:del_list' list_id list_slug %}" class="btn btn-sm btn-danger">Delete this list</a>

+ 0 - 41
tickets/templates/tickets/include/ticket_create.html

@@ -1,41 +0,0 @@
-<form method="post">
-  {% csrf_token %}
-    <div class="form-group">
-      <label class="mt-3" for="id_type">Type</label>    
-      {{ form.type }}
-    </div>
-
-    <div class="form-group">
-      <label for="id_title">Title</label>
-       {{ form.title }}
-    </div>
-
-    <div class="form-group">
-      <label for="id_note">Description</label>
-       {{ form.note }}
-    </div>
-
-    <div class="form-group">
-      <label for="id_due_date">Due Date</label>
-      {{ form.due_date }}
-    </div>
-
-    <div class="form-group">
-      <label for="id_assigned_to">Assigned To</label> 
-      {{ form.assigned_to }}
-    </div>
-
-    <div class="form-group">
-      <div class="form-check">
-        <input name="notify" class="form-check-input" type="checkbox" aria-describedby="inputNotifyHelp" checked="checked" id="id_notify">
-        <label class="form-check-label" for="id_notify">
-          Notify
-        </label>
-      </div>
-    </div>
-
-    <p>
-      <input type="submit" name="ticket_create" value="Create" class="btn btn-primary">
-    </p>
-  </div>
-</form>

+ 0 - 42
tickets/templates/tickets/include/ticket_edit.html

@@ -1,42 +0,0 @@
-<form class="mt-3" method="post">
-  {% csrf_token %}
-
-  <div class="form-group">
-    <label for="id_title">Title</label>
-    {{ form.title }}
-  </div>
-
-  <div class="form-group">
-    <div class="row">
-      <div class="col-12 col-md-6">
-        <label for="id_type">Type</label>    
-        {{ form.type }}
-      </div>
-      <div class="col-12 col-md-6">
-        <label for="id_state">State</label>
-        <select id="id_state" class="custom-select" name="state">
-          {% for state in states_list %}
-            <option value="{{ state }}" {% if task.status == state %}selected{% endif %}>{{ state }}</option>
-          {% endfor %}
-        </select>
-      </div>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label for="id_note">Note</label>
-    {{ form.note }}
-  </div>
-
-  <div class="form-group">
-    <label for="id_due_date">Due Date</label>
-    {{ form.due_date }}
-  </div>
-
-  <div class="form-group">
-    <label for="id_assigned_to">Assigned To</label>
-    {{ form.assigned_to }}
-  </div>
-
-  <input type="submit" name="ticket_edit" value="Edit" class="btn btn-primary">
-</form>

+ 0 - 109
tickets/templates/tickets/list_detail.html

@@ -1,109 +0,0 @@
-{% extends "tickets/base.html" %}
-{% load static %}
-
-{% block title %}tickets List: {{ task_list.name }}{% endblock %}
-
-{% block content %}
-  {% if list_slug != "mine" %}
-    <button class="btn btn-primary" id="addTicketBtn" type="button"
-      data-toggle="collapse" data-target="#editTicket">Add ticket</button>
-
-    {# Task edit / new task form #}
-    <div id="editTicket" class="collapse">
-      {% include 'tickets/include/ticket_create.html' %}
-    </div>
-    <hr>
-  {% endif %}
-
-  {% if tickets %}
-    {% if list_slug == "mine" %}
-      <h1>Tickets assigned to me (in all groups)</h1>
-    {% else %}
-      <h1>Tickets in "{{ task_list.name }}"</h1>
-      <p><small><i>In workgroup "{{ task_list.group }}" - drag rows to set priorities.</i></small></p>
-    {% endif %}
-
-    <table class="table" id="tasktable">
-      <tr class="nodrop">
-        <th>ID</th>
-        <th>Title</th>
-        <th>Created</th>
-        <th>Due on</th>
-        <th>Owner</th>
-        <th>Assigned</th>
-        <th>Type</th>
-        <th>State</th>
-      </tr>
-
-      {% for ticket in tickets %}
-        <tr class="item" id="{{ ticket.pk }}">
-          <td>
-              {{ ticket.pk }}
-          </td>
-          <td>
-              <a href="{% url 'tickets:task_detail' ticket.pk %}">{{ ticket.title|truncatewords:10 }}</a>
-          </td>
-          <td>
-              {{ ticket.created_date|date:"d.m.Y" }}
-          </td>
-          <td>
-            <span {% if task.overdue_status %}class="overdue"{% endif %}>
-              {{ ticket.due_date|date:"d.m.Y" }}
-            </span>
-          </td>
-          <td>
-            {{ ticket.created_by }}
-          </td>
-          <td>
-            {% if ticket.assigned_to %}{{ ticket.assigned_to }}{% else %}Anyone{% endif %}
-          </td>
-          <td>{{ ticket.type|title }}</td>
-          <td>{{ ticket.status }}</td>
-        </tr>
-      {% endfor %}
-    </table>
-   
-
-    {% if list_slug != "mine" %}
-      {% include 'tickets/include/delete_list.html' %}
-    {% endif %}
-
-  {% else %}
-    <h4>No tickets on this list yet!</h4>
-    <hr>
-    {% include 'tickets/include/delete_list.html' %}
-  {% endif %}
-
-{% endblock %}
-
-{% block extra_js %}
-  <script src="{% static 'tickets/js/jquery.tablednd_0_5.js' %}" type="text/javascript"></script>
-
-  <script type="text/javascript">
-    // Default
-    if (document.getElementById('addTicketBtn') != null) {
-      document.getElementById('addTicketBtn').onclick = function () {
-        handleClick(document.getElementById('addTicketBtn'), "Add ticket", "Cancel");
-      };
-    }
-
-    function order_tasks(data) {
-      // The JQuery plugin tableDnD provides a serialize() function which provides the re-ordered
-      // data in a list. We pass that list as an object ("data") to a Django view
-      // to save new priorities on each task in the list.
-      $.post("{% url 'tickets:reorder_tasks' %}", data, "json");
-      return false;
-    };
-
-    $(document).ready(function() {
-      // Initialise the task table for drag/drop re-ordering
-      $("#tasktable").tableDnD();
-
-      $('#tasktable').tableDnD({
-        onDrop: function(table, row) {
-          order_tasks($.tableDnD.serialize());
-        }
-      });
-    });
-  </script>
-{% endblock extra_js %}

+ 0 - 30
tickets/templates/tickets/list_lists.html

@@ -1,30 +0,0 @@
-{% extends "tickets/base.html" %}
-
-{% block title %}{{ list_title }} Lists{% endblock %}
-
-{% block content %}
-  <h1>Lists</h1>
-
-  <p><b>{{ ticket_count }}</b> tickets in {{ list_count }} list{{ list_count|pluralize }}</p>
-
-  {% regroup lists by group as section_list %}
-  {% for group in section_list %}
-    <h3>Group: {{ group.grouper }}</h3>
-    <ul class="list-group mb-4">
-      {% for list in group.list %}
-      <li class="list-group-item d-flex justify-content-between align-items-center">
-        <a href="{% url 'tickets:list_detail' list.id list.slug %}">{{ list.name }}</a>
-        <span class="badge badge-primary badge-pill"><b>{{ list.ticket_count }}</b></span>
-      </li>
-      {% endfor %}
-    </ul>
-  {% endfor %}
-
-  <div class="mt-3">
-    {% if user.is_staff %}
-      <a href="{% url 'tickets:create_list' %}" class="btn btn-primary">Create new list</a>
-    {% else %}
-      <a href="" class="btn btn-primary disabled">If you were staff, you could create a new list</a>
-    {% endif %}
-  </div>
-{% endblock %}

+ 0 - 29
tickets/templates/tickets/search_results.html

@@ -1,29 +0,0 @@
-{% extends "tickets/base.html" %}
-
-{% block title %}Search results{% endblock %}
-{% block content_title %}<h2 class="page_title">Search</h2>{% endblock %}
-
-{% block content %}
-  {% if found_tasks %}
-  <h2>{{found_tasks.count}} search results for term: "{{ query_string }}"</h2>
-  <div class="post_list">
-    {% for f in found_tasks %}
-    <p>
-      <strong>
-        <a href="{% url 'tickets:task_detail' f.id %}">{{ f.title }}</a>
-      </strong>
-      <br />
-      <span class="minor">
-        In list:
-        <a href="{% url 'tickets:list_detail' f.task_list.id f.task_list.slug %}">
-          {{ f.task_list.name }}
-        </a>
-        <br /> Assigned to: {% if f.assigned_to %}{{ f.assigned_to }}{% else %}Anyone{% endif %}
-      </span>
-    </p>
-    {% endfor %}
-  </div>
-  {% else %}
-    <h2> No results to show, sorry.</h2>
-  {% endif %}
-{% endblock %}

+ 0 - 188
tickets/templates/tickets/task_detail.html

@@ -1,188 +0,0 @@
-{% extends "tickets/base.html" %}
-
-{% block title %}Task:{{ task.title }}{% endblock %}
-
-{% block extrahead %}
-<style>
-.select2 {
-    width: 100% !important;
-}
-
-.select2-container {
-    min-width: 0 !important;
-}
-</style>
-{{ form.media }}
-{{ merge_form.media }}
-{% endblock %}
-
-
-
-{% block content %}
-  <div class="card-deck">
-    <div class="card col-sm-8">
-      <div class="card-body">        
-        <h3 class="card-title">{{ task.title }}</h3>
-        {% if task.note %}
-        <div class="card-text">{{ task.note|safe|urlize|linebreaks }}</div>
-        {% endif %}
-      </div>
-    </div>
-
-    <div class="card col-sm-4 p-0">
-      <ul class="list-group list-group-flush">
-        <li class="list-group-item">
-          <strong>Type:</strong> {{ task.type|title }}
-        </li>
-        <li class="list-group-item">
-          <strong>State:</strong> {{ task.status }}
-        </li>
-        <li class="list-group-item">
-          <strong>Assigned to:</strong> {% if task.assigned_to %} {{ task.assigned_to }} {% else %} Anyone {% endif %}
-        </li>
-        <li class="list-group-item">
-          <strong>Created by:</strong> {{ task.created_by }}
-        </li>
-        <li class="list-group-item">
-          <strong>Due date:</strong> {{ task.due_date }}
-        </li>
-        <li class="list-group-item">
-          <strong>State changed on:</strong> {{ task.status_changed_date }}
-        </li>
-        <li class="list-group-item">
-          <button
-            class="btn btn-sm btn-primary"
-            id="editTaskButton"
-            type="button"
-            data-toggle="collapse"
-            data-target="#ticket_edit">Edit</button>
-
-          <form method="post" action="{% url 'tickets:delete_task' task.id %}" role="form" class="d-inline">
-            {% csrf_token %}
-            <div style="display:inline;">
-              <button class="btn btn-danger btn-sm" type="submit" onclick="return confirm('Are you sure you want to delete this task?');" name="submit_delete">
-                Delete
-              </button>
-            </div>
-          </form>
-        </li>
-      </ul>
-    </div>
-  </div>
-
-  <div id="ticket_edit" class="collapse">
-    {% include 'tickets/include/ticket_edit.html' %}
-  </div>
-
-{% if attachments_enabled %}
-  <div class="card mt-4">
-    <h5 class="card-header">
-      Attachments
-    </h5>
-
-    <div class="card-body pb-0">
-      {% if task.attachment_set.count %}
-        <div class="table-responsive">
-          <table class="table mb-3">
-            <thead>
-              <tr>
-                <th>File</th>
-                <th>Uploaded</th>
-                <th>By</th>
-                <th>Type</th>
-                <th>Remove</th>
-              </tr>
-            </thead>
-            <tbody>
-              {% for attachment in task.attachment_set.all %}
-                <tr>
-                  <td><a href="{{ attachment.file.url }}">{{ attachment.filename }}</a></td>
-                  <td>{{ attachment.timestamp }}</td>
-                  <td>{{ attachment.added_by.get_full_name }}</td>
-                  <td>{{ attachment.extension.lower }}</td>
-                  <td>
-                    <form action="{% url "tickets:remove_attachment" attachment.id %}" method="POST">
-                      {% csrf_token %}
-                      <input type="submit" value="X" class="btn btn-danger btn-sm">
-                    </form>
-                  </td>
-                </tr>
-              {% endfor %}
-            </tbody>
-          </table>
-        </div>
-      {% endif %}
-
-      <form method="POST" action="" enctype="multipart/form-data" style="width:50%;">
-        {% csrf_token %}
-        <div class="input-group mb-3">
-          <div class="custom-file">
-            <input type="file" class="custom-file-input" id="attachment_file_input" name="attachment_file_input" />
-            <label class="custom-file-label" for="attachment_file_input">Choose file</label>
-          </div>
-          <div class="input-group-append">
-            <button class="btn btn-primary">Upload</button>
-          </div>
-        </div>
-      </form>
-    </div>
-  </div>
-{% endif %}
-
-  <div class="mt-3">
-    <h5>Add comment</h5>
-    <form action="" method="post">
-      {% csrf_token %}
-      <div class="form-group">
-        <textarea class="form-control" name="comment-body" rows="3" required></textarea>
-      </div>
-      <input class="btn btn-sm btn-primary" type="submit" name="add_comment" value="Add Comment">
-    </form>
-  </div>
-
-  <div class="task_comments mt-4">
-    {% if comment_list %}
-      <h5>Comments on this task</h5>
-      {% for comment in comment_list %}
-      <div class="mb-3 card">
-        <div class="card-header">
-          <div class="float-left">
-            {% if comment.email_message_id %}
-            <span class="badge badge-warning">email</span>
-            {% endif %}
-            {{ comment.author_text }}
-          </div>
-          <span class="float-right d-inline-block text-muted">
-            {{ comment.date|date:"F d Y P" }}
-          </span>
-        </div>
-        <div class="{{ comment_classes | join:" " }} card-body">
-          {{ comment.body|safe|urlize|linebreaks }}
-        </div>
-      </div>
-      {% endfor %}
-    {% else %}
-        <h5>No comments (yet).</h5>
-    {% endif %}
-  </div>
-{% endblock %}
-
-{% block extra_js %}
-  <script>
-    let editTaskButton = document.getElementById('editTaskButton');
-    if (editTaskButton != null) {
-      editTaskButton.onclick = function () {
-        handleClick(editTaskButton, "Edit", "Cancel");
-      };
-    }
-
-    // Support file attachment uploader
-    $('#attachment_file_input').on('change',function(){
-      // Get the file name and remove browser-added "fakepath."
-      // Then replace the "Choose a file" label.
-      var fileName = $(this).val().replace('C:\\fakepath\\', " ");
-      $(this).next('.custom-file-label').html(fileName);
-    })
-  </script>
-{% endblock extra_js %}
-

+ 0 - 36
tickets/urls.py

@@ -1,36 +0,0 @@
-from django.urls import path, include
-from tickets import views
-from .apiviews import *
-from rest_framework import routers
-
-router = routers.SimpleRouter()
-router.register(r'tickets', TaskMVS)
-router.register(r'list', TaskListMVS)
-router.register(r'type', TicketTypeMVS)
-router.register(r'comment', CommentMVS)
-router.register(r'attachments', AttachmentMVS)
-
-app_name = "tickets"
-
-urlpatterns = [
-    path("", views.list_lists, name="lists"),
-    path("reorder_tasks/", views.reorder_tasks, name="reorder_tasks"),
-    path("mine/", views.list_detail, {"list_slug": "mine"}, name="mine"),
-    path("<int:list_id>/<str:list_slug>/", views.list_detail, name="list_detail"),
-    path("<int:list_id>/<str:list_slug>/delete/", views.del_list, name="del_list"),
-    path("create_list/", views.create_list, name="create_list"),
-    path("task/<int:task_id>/", views.task_detail, name="task_detail"),
-    path(
-        "attachment/remove/<int:attachment_id>/", views.remove_attachment, name="remove_attachment"
-    ),
-    path("api/", include(router.urls))
-]
-
-urlpatterns.extend(
-    [
-        path("change_status/<int:task_id>/", views.change_status, name="task_change_status"),
-        path("delete/<int:task_id>/", views.delete_task, name="delete_task"),
-        path("search/", views.search, name="search"),
-        # path("import_csv/", views.import_csv, name="import_csv")
-    ]
-)

+ 0 - 198
tickets/utils.py

@@ -1,198 +0,0 @@
-import email.utils
-import logging
-import os
-import time
-
-from django.conf import settings
-from django.contrib.sites.models import Site
-from django.core import mail
-from django.template.loader import render_to_string
-
-
-from tickets.defaults import defaults
-from tickets.models import Attachment, Comment, Task
-
-log = logging.getLogger(__name__)
-
-
-def staff_check(user):
-    """If tickets_STAFF_ONLY is set to True, limit view access to staff users only.
-        # FIXME: More granular access control needed - see
-        https://github.com/shacker/django-todo/issues/50
-    """
-
-    if defaults("TICKETS_STAFF_ONLY"):
-        return user.is_staff
-    else:
-        # If unset or False, allow all logged in users
-        return True
-
-
-def user_can_read_task(task, user):
-    return task.task_list.group in user.groups.all() or user.is_superuser
-
-
-def tickets_get_backend(task):
-    """Returns a mail backend for some task"""
-    mail_backends = getattr(settings, "TICKETS_MAIL_BACKENDS", None)
-    print(mail_backends)
-    if mail_backends is None:
-        return None
-
-    #task_backend = mail_backends[task.task_list.slug]
-    task_backend = mail_backends['mail-queue']
-    if task_backend is None:
-        return None
-
-    return task_backend
-
-def tickets_get_mailer(user, task):
-    """A mailer is a (from_address, backend) pair"""
-    task_backend = tickets_get_backend(task)
-    if task_backend is None:
-        return (None, mail.get_connection)
-
-    from_address = getattr(task_backend, "from_address")
-    from_address = email.utils.formataddr((user.username, from_address))
-    return (from_address, task_backend)
-
-
-def tickets_send_mail(user, task, subject, body, recip_list):
-    """Send an email attached to task, triggered by user"""
-    if settings.IS_SEND_EMAIL:
-        references = Comment.objects.filter(task=task).only("email_message_id")
-        references = (ref.email_message_id for ref in references)
-        references = " ".join(filter(bool, references))
-
-        from_address, backend = tickets_get_mailer(user, task)
-        message_hash = hash((subject, body, from_address, frozenset(recip_list), references))
-
-        message_id = (
-            # the task_id enables attaching back notification answers
-            "<notif-{task_id}."
-            # the message hash / epoch pair enables deduplication
-            "{message_hash:x}."
-            "{epoch}@django-tickets>"
-        ).format(
-            task_id=task.pk,
-            # avoid the -hexstring case (hashes can be negative)
-            message_hash=abs(message_hash),
-            epoch=int(time.time()),
-        )
-
-        # the thread message id is used as a common denominator between all
-        # notifications for some task. This message doesn't actually exist,
-        # it's just there to make threading possible
-        thread_message_id = "<thread-{}@django-tickets>".format(task.pk)
-        references = "{} {}".format(references, thread_message_id)
-
-        with backend() as connection:
-            message = mail.EmailMessage(
-                subject,
-                body,
-                from_address,
-                recip_list,
-                [],  # Bcc
-                headers={
-                    **getattr(backend, "headers", {}),
-                    "Message-ID": message_id,
-                    "References": references,
-                    "In-reply-to": thread_message_id,
-                },
-                connection=connection,
-            )
-            message.send()
-
-def send_notify_mail(new_task):
-    """
-    Send email to assignee if task is assigned to someone other than submittor.
-    Unassigned tasks should not try to notify.
-    """
-
-    if new_task.assigned_to == new_task.created_by:
-        return
-
-    current_site = Site.objects.get_current()
-    subject = render_to_string("tickets/email/assigned_subject.txt", {"task": new_task})
-    body = render_to_string(
-        "tickets/email/assigned_body.txt", {"task": new_task, "site": current_site}
-    )
-
-    recip_list = [new_task.assigned_to.email]
-    tickets_send_mail(new_task.created_by, new_task, subject, body, recip_list)
-
-def send_notify_change_mail(new_task, user):
-    """
-    Send email to assignee if task is assigned to someone other than submittor.
-    Unassigned tasks should not try to notify.
-    """
-
-    if new_task.assigned_to == new_task.created_by:
-        return
-
-    current_site = Site.objects.get_current()
-    subject = render_to_string("tickets/email/assigned_subject.txt", {"task": new_task})
-    body = render_to_string(
-        "tickets/email/changetask_body.txt", {"task": new_task, "site": current_site}
-    )
-
-    recip_list = [new_task.assigned_to.email]
-    if user == new_task.created_by:
-        tickets_send_mail(new_task.created_by, new_task, subject, body, recip_list)
-    else:
-        tickets_send_mail(user, new_task, subject, body, recip_list)
-        tickets_send_mail(user, new_task, subject, body, [new_task.created_by.email])
-
-
-def send_email_to_thread_participants(task, msg_body, user, subject=None):
-    """Notify all previous commentors on a Task about a new comment."""
-
-    current_site = Site.objects.get_current()
-    email_subject = subject
-    if not subject:
-        subject = render_to_string("tickets/email/assigned_subject.txt", {"task": task})
-
-    email_body = render_to_string(
-        "tickets/email/newcomment_body.txt",
-        {"task": task, "body": msg_body, "site": current_site, "user": user},
-    )
-
-    # Get all thread participants
-    commenters = Comment.objects.filter(task=task)
-    recip_list = set(ca.author.email for ca in commenters if ca.author is not None)
-    for related_user in (task.created_by, task.assigned_to):
-        if related_user is not None:
-            recip_list.add(related_user.email)
-    recip_list = list(m for m in recip_list if m)
-    if user == task.created_by:
-        tickets_send_mail(user, task, email_subject, email_body, recip_list)
-    else:
-        tickets_send_mail(user, task, email_subject, email_body, recip_list)
-        tickets_send_mail(user, task, email_subject, email_body, [task.created_by.email])
-
-def change_task_status(task_id: int, new_status: str) -> bool:
-    """Change task status"""
-    try:
-        task = Task.objects.get(id=task_id)
-        task.status = new_status
-        task.save()
-        return True
-        
-    except Task.DoesNotExist:
-        log.info(f"Task {task_id} not found.")
-        return False
-
-def remove_attachment_file(attachment_id: int) -> bool:
-    """Delete an Attachment object and its corresponding file from the filesystem."""
-    try:
-        attachment = Attachment.objects.get(id=attachment_id)
-        if attachment.file:
-            if os.path.isfile(attachment.file.path):
-                os.remove(attachment.file.path)
-
-        attachment.delete()
-        return True
-
-    except Attachment.DoesNotExist:
-        log.info(f"Attachment {attachment_id} not found.")
-        return False

+ 0 - 11
tickets/views/__init__.py

@@ -1,11 +0,0 @@
-from tickets.views.create_list import create_list  # noqa: F401
-from tickets.views.del_list import del_list  # noqa: F401
-from tickets.views.delete_task import delete_task  # noqa: F401
-from tickets.views.import_csv import import_csv  # noqa: F401
-from tickets.views.list_detail import list_detail  # noqa: F401
-from tickets.views.list_lists import list_lists  # noqa: F401
-from tickets.views.remove_attachment import remove_attachment  # noqa: F401
-from tickets.views.reorder_tasks import reorder_tasks  # noqa: F401
-from tickets.views.search import search  # noqa: F401
-from tickets.views.task_detail import task_detail  # noqa: F401
-from tickets.views.change_status import change_status # noqa: F401

+ 0 - 42
tickets/views/change_status.py

@@ -1,42 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404, redirect
-from django.urls import reverse
-
-from tickets.models import Task
-from tickets.utils import change_task_status
-from tickets.utils import staff_check
-
-@login_required
-@user_passes_test(staff_check)
-def change_status(request, task_id: int) -> HttpResponse:
-    """Toggle the completed status of a task from done to undone, or vice versa.
-    Redirect to the list from which the task came.
-    """
-
-    if request.method == "POST":
-        task = get_object_or_404(Task, pk=task_id)
-
-        redir_url = reverse(
-            "tickets:list_detail",
-            kwargs={"list_id": task.task_list.id, "list_slug": task.task_list.slug},
-        )
-
-        # Permissions
-        if not (
-            (task.created_by == request.user)
-            or (request.user.is_superuser)
-            or (task.assigned_to == request.user)
-            or (task.task_list.group in request.user.groups.all())
-        ):
-            raise PermissionDenied
-
-        change_task_status(task.id, request.POST["select-status"])
-        messages.success(request, "Task status changed for '{}'".format(task.title))
-
-        return redirect(redir_url)
-
-    else:
-        raise PermissionDenied

+ 0 - 47
tickets/views/create_list.py

@@ -1,47 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.core.exceptions import PermissionDenied
-from django.db import IntegrityError
-from django.http import HttpResponse
-from django.shortcuts import redirect, render
-from django.utils.text import slugify
-
-from tickets.forms import ListCreateForm
-from tickets.utils import staff_check
-
-
-@login_required
-@user_passes_test(staff_check)
-def create_list(request) -> HttpResponse:
-    """Allow users to add a new tickets list to the group they're in.
-    """
-
-    # Only staffers can add lists, regardless of tickets_STAFF_USER setting.
-    if not request.user.is_staff:
-        raise PermissionDenied
-
-    if request.POST:
-        form = ListCreateForm(request.user, request.POST)
-        if form.is_valid():
-            try:
-                newlist = form.save(commit=False)
-                newlist.slug = slugify(newlist.name, allow_unicode=True)
-                newlist.save()
-                messages.success(request, "A new list has been added.")
-                return redirect("tickets:lists")
-
-            except IntegrityError:
-                messages.warning(
-                    request,
-                    "There was a problem saving the new list. "
-                    "Most likely a list with the same name in the same group already exists.",
-                )
-    else:
-        if request.user.groups.count() == 1:
-            form = ListCreateForm(request.user, initial={"group": request.user.groups.first()})
-        else:
-            form = ListCreateForm(request.user)
-
-    context = {"form": form}
-
-    return render(request, "tickets/create_list.html", context)

+ 0 - 34
tickets/views/del_list.py

@@ -1,34 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404, redirect, render
-
-from tickets.models import TaskList
-from tickets.utils import staff_check
-
-
-@login_required
-@user_passes_test(staff_check)
-def del_list(request, list_id: int, list_slug: str) -> HttpResponse:
-    """Delete an entire list. Only staff members should be allowed to access this view.
-    """
-    task_list = get_object_or_404(TaskList, id=list_id)
-
-    # Ensure user has permission to delete list. Get the group this list belongs to,
-    # and check whether current user is a member of that group AND a staffer.
-    if task_list.group not in request.user.groups.all():
-        raise PermissionDenied    
-    if not request.user.is_staff:
-        raise PermissionDenied
-
-    if request.method == "POST":
-        TaskList.objects.get(id=task_list.id).delete()
-        messages.success(request, "{list_name} is gone.".format(list_name=task_list.name))
-        return redirect("tickets:lists")
-
-    context = {
-        "task_list": task_list
-    }
-
-    return render(request, "tickets/del_list.html", context)

+ 0 - 42
tickets/views/delete_task.py

@@ -1,42 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404, redirect
-from django.urls import reverse
-
-from tickets.models import Task
-from tickets.utils import staff_check
-
-
-@login_required
-@user_passes_test(staff_check)
-def delete_task(request, task_id: int) -> HttpResponse:
-    """Delete specified task.
-    Redirect to the list from which the task came.
-    """
-
-    if request.method == "POST":
-        task = get_object_or_404(Task, pk=task_id)
-
-        redir_url = reverse(
-            "tickets:list_detail",
-            kwargs={"list_id": task.task_list.id, "list_slug": task.task_list.slug},
-        )
-
-        # Permissions
-        if not (
-            (task.created_by == request.user)
-            or (request.user.is_superuser)
-            or (task.assigned_to == request.user)
-            or (task.task_list.group in request.user.groups.all())
-        ):
-            raise PermissionDenied
-
-        task.delete()
-
-        messages.success(request, "Task '{}' has been deleted".format(task.title))
-        return redirect(redir_url)
-
-    else:
-        raise PermissionDenied

+ 0 - 34
tickets/views/import_csv.py

@@ -1,34 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.http import HttpResponse
-from django.shortcuts import redirect, render, reverse
-
-from tickets.operations.csv_importer import CSVImporter
-from tickets.utils import staff_check
-
-
-@login_required
-@user_passes_test(staff_check)
-def import_csv(request) -> HttpResponse:
-    """Import a specifically formatted CSV into stored tasks.
-    """
-
-    ctx = {"results": None}
-
-    if request.method == "POST":
-        filepath = request.FILES.get("csvfile")
-
-        if not filepath:
-            messages.error(request, "You must supply a CSV file to import.")
-            return redirect(reverse("tickets:import_csv"))
-
-        importer = CSVImporter()
-        results = importer.upsert(filepath)
-
-        if results:
-            ctx["results"] = results
-        else:
-            messages.error(request, "Could not parse provided CSV file.")
-            return redirect(reverse("tickets:import_csv"))
-
-    return render(request, "tickets/import_csv.html", context=ctx)

+ 0 - 57
tickets/views/list_detail.py

@@ -1,57 +0,0 @@
-import bleach
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404, redirect, render
-
-from tickets.forms import TicketForm
-from tickets.models import Task, TaskList
-from tickets.utils import send_notify_mail, staff_check
-
-
-@login_required
-@user_passes_test(staff_check)
-def list_detail(request, list_id=None, list_slug=None) -> HttpResponse:
-    if list_slug == "mine":
-        tickets = Task.objects.filter(assigned_to=request.user).select_related("created_by", "assigned_to")
-        context = {
-                "tickets": tickets,
-                "list_id": list_id,
-                "list_slug": list_slug
-            }
-    else:
-        task_list = get_object_or_404(TaskList, id=list_id)
-        if task_list.group not in request.user.groups.all() and not request.user.is_superuser:
-            raise PermissionDenied
-        tickets = Task.objects.filter(task_list=task_list.id).select_related("created_by", "assigned_to")
-
-        form = TicketForm(request.user, request.POST, initial={"task_list": task_list})
-        if request.POST.getlist("ticket_create"):
-            if form.is_valid():
-                new_ticket = form.save(commit=False)
-                new_ticket.created_by = request.user
-                new_ticket.note = bleach.clean(form.cleaned_data["note"], strip=True)
-                new_ticket.task_list = task_list
-                new_ticket.status = new_ticket.first_state()
-                form.save()
-                
-                if (
-                    "notify" in request.POST
-                    and new_ticket.assigned_to
-                    and new_ticket.assigned_to != request.user
-                ):
-                    send_notify_mail(new_ticket)
-
-                messages.success(request, 'New ticket "{t}" has been added.'.format(t=new_ticket.title))
-                return redirect(request.path)
-
-        context = {
-            "form": form,
-            "list_id": list_id,
-            "list_slug": list_slug,
-            "task_list": task_list,
-            "tickets": tickets,
-        }
-
-    return render(request, "tickets/list_detail.html", context)

+ 0 - 42
tickets/views/list_lists.py

@@ -1,42 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.http import HttpResponse
-from django.shortcuts import render
-from django.db.models import Count
-
-from django.contrib.auth.models import Group, Permission
-from django.contrib.contenttypes.models import ContentType
-from tickets.forms import SearchForm
-from tickets.models import *
-from tickets.utils import staff_check
-from tickets.admin_utils import add_default_tickets_list
-
-@login_required
-@user_passes_test(staff_check)
-def list_lists(request) -> HttpResponse:
-    searchform = SearchForm(auto_id=False)
-    add_default_tickets_list(request)
-    # Make sure user belongs to at least one group.
-    if not request.user.groups.all().exists():
-        messages.warning(
-            request,
-            "You do not yet belong to any groups. Ask your administrator to add you to one.",
-        )
-
-    # Get info about Lists
-    lists = TaskList.objects.select_related("group").order_by("group__name", "name")
-    if not request.user.is_superuser:
-        lists = lists.filter(group__in=request.user.groups.all())
-    lists = lists.annotate(ticket_count=Count("task"))
-
-    # Get info about Tickets
-    tickets = Task.objects.filter(task_list__in=lists)
-
-    context = {
-        "lists": lists,
-        "ticket_count": len(tickets),
-        "list_count": len(lists),
-        "searchform": searchform
-    }
-
-    return render(request, "tickets/list_lists.html", context)

+ 0 - 40
tickets/views/remove_attachment.py

@@ -1,40 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required
-from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404, redirect
-from django.urls import reverse
-
-from tickets.models import Attachment
-from tickets.utils import remove_attachment_file
-
-
-@login_required
-def remove_attachment(request, attachment_id: int) -> HttpResponse:
-    """Delete a previously posted attachment object and its corresponding file
-    from the filesystem, permissions allowing.
-    """
-
-    if request.method == "POST":
-        attachment = get_object_or_404(Attachment, pk=attachment_id)
-
-        redir_url = reverse("tickets:task_detail", kwargs={"task_id": attachment.task.id})
-
-        # Permissions
-        if not (
-            attachment.task.task_list.group in request.user.groups.all()
-            or request.user.is_superuser
-        ):
-            raise PermissionDenied
-
-        if remove_attachment_file(attachment.id):
-            messages.success(request, f"Attachment {attachment.id} removed.")
-        else:
-            messages.error(
-                request, f"Sorry, there was a problem deleting attachment {attachment.id}."
-            )
-
-        return redirect(redir_url)
-
-    else:
-        raise PermissionDenied

+ 0 - 35
tickets/views/reorder_tasks.py

@@ -1,35 +0,0 @@
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.http import HttpResponse
-from django.views.decorators.csrf import csrf_exempt
-
-from tickets.models import Task
-from tickets.utils import staff_check
-
-
-@csrf_exempt
-@login_required
-@user_passes_test(staff_check)
-def reorder_tasks(request) -> HttpResponse:
-    """Handle task re-ordering (priorities) from JQuery drag/drop in list_detail.html
-    """
-    newtasklist = request.POST.getlist("tasktable[]")
-    if newtasklist:
-        # First task in received list is always empty - remove it
-        del newtasklist[0]
-
-        # Re-prioritize each task in list
-        i = 1
-        for id in newtasklist:
-            try:
-                task = Task.objects.get(pk=id)
-                task.priority = i
-                task.save()
-                i += 1
-            except Task.DoesNotExist:
-                # Can occur if task is deleted behind the scenes during re-ordering.
-                # Not easy to remove it from the UI without page refresh, but prevent crash.
-                pass
-
-    # All views must return an httpresponse of some kind ... without this we get
-    # error 500s in the log even though things look peachy in the browser.
-    return HttpResponse(status=201)

+ 0 - 43
tickets/views/search.py

@@ -1,43 +0,0 @@
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.db.models import Q
-from django.http import HttpResponse
-from django.shortcuts import render
-
-from tickets.models import Task
-from tickets.utils import staff_check
-
-
-@login_required
-@user_passes_test(staff_check)
-def search(request) -> HttpResponse:
-    """Search for tasks user has permission to see.
-    """
-
-    query_string = ""
-
-    if request.GET:
-
-        found_tasks = None
-        if ("q" in request.GET) and request.GET["q"].strip():
-            query_string = request.GET["q"]
-
-            found_tasks = Task.objects.filter(
-                Q(title__icontains=query_string) | Q(note__icontains=query_string)
-            )
-        else:
-            # What if they selected the "completed" toggle but didn't enter a query string?
-            # We still need found_tasks in a queryset so it can be "excluded" below.
-            found_tasks = Task.objects.all()
-
-        if "inc_complete" in request.GET:
-            found_tasks = found_tasks.exclude(completed=True)
-
-    else:
-        found_tasks = None
-
-    # Only include tasks that are in groups of which this user is a member:
-    if not request.user.is_superuser:
-        found_tasks = found_tasks.filter(task_list__group__in=request.user.groups.all())
-
-    context = {"query_string": query_string, "found_tasks": found_tasks}
-    return render(request, "tickets/search_results.html", context)

+ 0 - 114
tickets/views/task_detail.py

@@ -1,114 +0,0 @@
-import datetime
-import os
-
-import bleach
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required, user_passes_test
-from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404, redirect, render
-from tickets.queries import Query as q
-
-
-from tickets.defaults import defaults
-from tickets.forms import TicketForm
-from tickets.models import Attachment, Comment, Task
-
-from tickets.utils import (
-    send_email_to_thread_participants,
-    staff_check,
-    user_can_read_task,
-    send_notify_change_mail
-)
-
-
-def handle_add_comment(request, task):
-    if not request.POST.get("add_comment"):
-        return
-
-    Comment.objects.create(
-        author=request.user, task=task, body=bleach.clean(request.POST["comment-body"], strip=True)
-    )
-
-    send_email_to_thread_participants(
-        task,
-        request.POST["comment-body"],
-        request.user,
-        subject='New comment posted on task "{}"'.format(task.title),
-    )
-
-    messages.success(request, "Comment posted. Notification email sent to thread participants.")
-
-
-@login_required
-@user_passes_test(staff_check)
-def task_detail(request, task_id: int) -> HttpResponse:
-    """View task details. Allow task details to be edited. Process new comments on task.
-    """
-
-    ticket = get_object_or_404(Task.objects.select_related("created_by", "assigned_to"), pk=task_id)
-    comment_list = Comment.objects.filter(task=task_id).order_by("-date")
-    states_list = ticket.now_state_list()
-    #q.get_tickets()
-
-    # Ensure user has permission to view task. Superusers can view all tasks.
-    # Get the group this task belongs to, and check whether current user is a member of that group.
-    if not user_can_read_task(ticket, request.user):
-        raise PermissionDenied
-
-    # Save submitted comments
-    handle_add_comment(request, ticket)
-
-    # Save task edits
-    if not request.POST.get("ticket_edit"):
-        form = TicketForm(request.user, instance=ticket, initial={"task_list": ticket.task_list, "type_disabled": True})
-    else:
-        form = TicketForm(request.user, request.POST, instance=ticket, initial={"task_list": ticket.task_list, "type_disabled": True})
-        
-        if form.is_valid():
-            edited_ticket = form.save(commit=False)
-            edited_ticket.note = bleach.clean(form.cleaned_data["note"], strip=True)
-            edited_ticket.title = bleach.clean(form.cleaned_data["title"], strip=True)    
-            edited_ticket.status = request.POST.get("state")
-            edited_ticket.save()
-            print(ticket)
-            send_notify_change_mail(ticket, request.user)
-            messages.success(request, "The task has been edited.")
-            return redirect("tickets:list_detail", list_id=ticket.task_list.id, list_slug=ticket.task_list.slug)
-
-    if ticket.due_date:
-        thedate = ticket.due_date
-    else:
-        thedate = datetime.datetime.now()
-
-    # Handle uploaded files
-    if request.FILES.get("attachment_file_input"):
-        file = request.FILES.get("attachment_file_input")
-
-        if file.size > defaults("TICKETS_MAXIMUM_ATTACHMENT_SIZE"):
-            messages.error(request, f"File exceeds maximum attachment size.")
-            return redirect("tickets:task_detail", task_id=ticket.id)
-
-        name, extension = os.path.splitext(file.name)
-
-        if extension not in defaults("TICKETS_LIMIT_FILE_ATTACHMENTS"):
-            messages.error(request, f"This site does not allow upload of {extension} files.")
-            return redirect("tickets:task_detail", task_id=ticket.id)
-
-        Attachment.objects.create(
-            task=ticket, added_by=request.user, timestamp=datetime.datetime.now(), file=file
-        )
-        messages.success(request, f"File attached successfully")
-        return redirect("tickets:task_detail", task_id=ticket.id)
-
-    context = {
-        "task": ticket,
-        "states_list": states_list,
-        "comment_list": comment_list,
-        "form": form,
-        "thedate": thedate,
-        "comment_classes": defaults("TICKETS_COMMENT_CLASSES"),
-        "attachments_enabled": defaults("TICKETS_ALLOW_FILE_ATTACHMENTS"),
-    }
-
-    return render(request, "tickets/task_detail.html", context)