diff --git a/project/callback/__init__.py b/project/callback/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/callback/admin.py b/project/callback/admin.py new file mode 100644 index 0000000..d4be419 --- /dev/null +++ b/project/callback/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin +from.models import ReqAvail + + +def user_name(obj): + if obj.user: + if obj.user.profile: + return obj.user.profile + else: + return obj.user.email + else: + return None +user_name.short_description = 'email' + + +class ReqAvailAdmin(admin.ModelAdmin): + change_form_template = 'admin/callback/callback_change.html' + list_display = (user_name, 'created_at', 'updated_at') + exclude = ('user',) + + +admin.site.register(ReqAvail, ReqAvailAdmin) + diff --git a/project/callback/context_processors.py b/project/callback/context_processors.py new file mode 100644 index 0000000..9e1c685 --- /dev/null +++ b/project/callback/context_processors.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from .forms import ReqAvailForm + + +def add_forms(request): + return { + 'reqf': ReqAvailForm(request.GET or None), + } diff --git a/project/callback/forms.py b/project/callback/forms.py new file mode 100644 index 0000000..e10254b --- /dev/null +++ b/project/callback/forms.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from django import forms + +from . import models + + +class ReqAvailForm(forms.ModelForm): + """Форма заказа товара.""" + class Meta: + model = models.ReqAvail + fields = ('name', 'phone', 'message') + + def __init__(self, *args, **kwargs): + super(ReqAvailForm, self).__init__(*args, **kwargs) + self.fields['name'].label = u'Ваше имя' + self.fields['name'].widget.attrs['placeholder'] = u'Ваше имя, отчество' + self.fields['phone'].widget.attrs['placeholder'] = u'Ваш номер телефона или e-mail' + self.fields['name'].widget.attrs['width'] = u'Ваше имя, отчество' + + def clean(self): + super(ReqAvailForm, self).clean() + name = self.cleaned_data.get('name', '').strip() + phone = self.cleaned_data.get('phone', '').strip() + + if not name: + raise forms.ValidationError(u'Укажите Ваше имя.') + + if not phone: + raise forms.ValidationError(u'Укажите Ваш телефон.') + + return self.cleaned_data diff --git a/project/callback/migrations/0001_initial.py b/project/callback/migrations/0001_initial.py new file mode 100644 index 0000000..810cb71 --- /dev/null +++ b/project/callback/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'ReqAvail' + db.create_table(u'callback_reqavail', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('phone', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('message', self.gf('django.db.models.fields.TextField')()), + ('status', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=0)), + ('status_changed', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal(u'callback', ['ReqAvail']) + + + def backwards(self, orm): + # Deleting model 'ReqAvail' + db.delete_table(u'callback_reqavail') + + + models = { + u'callback.reqavail': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'ReqAvail'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'status': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}), + 'status_changed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['callback'] \ No newline at end of file diff --git a/project/callback/migrations/0002_auto__add_field_reqavail_user.py b/project/callback/migrations/0002_auto__add_field_reqavail_user.py new file mode 100644 index 0000000..635b155 --- /dev/null +++ b/project/callback/migrations/0002_auto__add_field_reqavail_user.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'ReqAvail.user' + db.add_column(u'callback_reqavail', 'user', + self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='messages', null=True, to=orm['auth.User']), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'ReqAvail.user' + db.delete_column(u'callback_reqavail', 'user_id') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'callback.reqavail': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'ReqAvail'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'status': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}), + 'status_changed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'messages'", 'null': 'True', 'to': u"orm['auth.User']"}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['callback'] \ No newline at end of file diff --git a/project/callback/migrations/__init__.py b/project/callback/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/callback/models.py b/project/callback/models.py new file mode 100644 index 0000000..df683e1 --- /dev/null +++ b/project/callback/models.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import os.path + +from django.db import models +from django.db.models import Q + +from django.contrib.auth.models import User + + +class ReqAvail(models.Model): + """Заказ товара.""" + NEW_STATUS = 0 + DONE_STATUS = 1 + + STATUS_CHOICES = ( + (NEW_STATUS, u'Новый'), + (DONE_STATUS, u'Обработан'), + ) + + name = models.CharField(u'Имя клиента', max_length=100) + phone = models.CharField(u'Телефон или e-mail', max_length=50) + message = models.TextField(u'Текст сообщения') + user = models.ForeignKey(User, related_name='messages', null=True, blank=True) +# email = models.EmailField(u'E-mail', default='', blank=True) +# region = models.ForeignKey(Region, verbose_name=u"Регион", blank=False, null=False, default='') + + #good = models.ForeignKey(CatalogItem, verbose_name=u'Товар', null=True, blank=True) + + status = models.PositiveSmallIntegerField(u'Статус', choices=STATUS_CHOICES, default=NEW_STATUS) + status_changed = models.DateTimeField(u'Статус изменен', null=True, blank=True, editable=False) + + created_at = models.DateTimeField(u'Дата заказа', auto_now_add=True) + updated_at = models.DateTimeField(u'Изменен', auto_now=True) + + class Meta: + verbose_name = u'Обратная связь' + verbose_name_plural = u'Обратная связь' + ordering = ['-created_at',] + + def __unicode__(self): + return u'Сообщение %s от %s' % ( + #self.good if self.good_id else u'(Ничего)', + self.name[:20], + self.created_at.strftime("%d.%m.%Y %H:%M"), + ) diff --git a/project/callback/urls.py b/project/callback/urls.py new file mode 100644 index 0000000..86289cd --- /dev/null +++ b/project/callback/urls.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from django.conf.urls.defaults import patterns, url +from .views import req_avail + +urlpatterns = patterns('', + url(r'^send/$', req_avail, name='callback-send-message'), + url(r'^send/(?P\d+)/$', req_avail, name='callback-request-item'), + ) diff --git a/project/callback/views.py b/project/callback/views.py new file mode 100644 index 0000000..aed6b9c --- /dev/null +++ b/project/callback/views.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import json + +from django.http import HttpResponseBadRequest, HttpResponse +from django.views.decorators.http import require_POST, require_GET +from django.views.decorators.csrf import csrf_protect +from django.template.loader import render_to_string +from django.core.mail import send_mail + +from django.conf import settings + +from .models import ReqAvail +from .forms import ReqAvailForm + +CALLBACK_SETTINGS = getattr(settings, 'CALLBACK_SETTINGS') + + +@require_POST +@csrf_protect +def req_avail(request, id=None): + """Отправить сообщение или обработать запрос наличия товара на складе - AJAX.""" + if not request.is_ajax(): + return HttpResponseBadRequest() + + if id: + good = get_object_or_404(CatalogItem, pk=id) + else: + good = None + form = ReqAvailForm(data=request.POST) + + if form.is_valid(): + if request.user.is_authenticated(): + user = request.user + else: + user = None + req = ReqAvail.objects.create( + name=form.cleaned_data['name'].strip(), + phone=form.cleaned_data['phone'].strip(), + user=user, + #good=good, + ) + + # отправить письма менеджерам + subject = CALLBACK_SETTINGS['NEW_REQ_AVAIL_EMAIL_SUBJ'] + sender = CALLBACK_SETTINGS['EMAIL_SENDER'] + recipients = CALLBACK_SETTINGS['MANAGERS_EMAILS'] + message = render_to_string('callback/req_avail_new.txt', {'req': req, }) + send_mail(subject, message, sender, recipients) + + data = { + 'success': form.is_valid(), +# 'field_errors': form.errors, # ошибки полей + 'form_errors': form.non_field_errors(), # ошибки формы + } + if form.is_valid(): + data['message'] = { + 'title': u'Сообщение', + 'msg': u'Ваша Сообщение отправлено!
Скоро с Вами свяжется наш менеджер.', + } + return HttpResponse(json.dumps(data), mimetype='application/json') + diff --git a/project/docs/consts.py b/project/docs/consts.py index f44d913..cb3564a 100644 --- a/project/docs/consts.py +++ b/project/docs/consts.py @@ -144,3 +144,4 @@ TAX_TYPE = ( (u'ИШ', u'ИШ - иные штрафы, установленные соответствующими нормативными актами'), ( u'0', u'0 - Конкретное значение указать невозможно'), ) + diff --git a/project/docs/models/invoice.py b/project/docs/models/invoice.py index 80b01be..ab889b4 100644 --- a/project/docs/models/invoice.py +++ b/project/docs/models/invoice.py @@ -20,6 +20,15 @@ class Invoice(BaseInvoiceModel): paid_status = models.PositiveSmallIntegerField(u'Оплачен?', choices=PAID_CHOICES, default=UNPAID) closed_status = models.BooleanField(u'Закрыт?', choices=consts.BOOL_CHOICES, default=False) + def docs_for_invoice(self): + doc_list = ['Faktura', 'Nakladn', 'AktRabot', ] + client_docs = {} + for doc in doc_list: + docs = models.get_model('docs', doc).objects.filter(invoice=self) + if docs: + client_docs[doc] = docs + return client_docs + class Meta(BaseInvoiceModel.Meta): verbose_name = u'Счёт' verbose_name_plural = u'Счета' diff --git a/project/settings.py b/project/settings.py index fb257ec..9cb1d17 100644 --- a/project/settings.py +++ b/project/settings.py @@ -129,6 +129,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.static', 'cms.context_processors.cms_settings', 'sekizai.context_processors.sekizai', + 'project.callback.context_processors.add_forms', ) @@ -161,6 +162,7 @@ INSTALLED_APPS = ( 'mptt', # utilities for implementing a modified pre-order traversal tree 'menus', # helper for model independent hierarchical website navigation 'sekizai', # for javascript and css management + 'captcha', #'robokassa', 'djangocms_admin_style', # for the admin skin. You **must** add 'djangocms_admin_style' in the list before 'django.contrib.admin'. 'django.contrib.messages', # to enable messages framework (see :ref:`Enable messages `) @@ -183,6 +185,7 @@ INSTALLED_APPS = ( 'project.customer', 'project.docs', 'project.pages', + 'project.callback', # keep it last 'south', @@ -252,6 +255,15 @@ ROBOKASSA_TEST_MODE = True from datetime import timedelta +CALLBACK_SETTINGS = { + 'EMAIL_SENDER': 'whosondutytoday@yandex.ru', + 'CHIEF_EMAILS': ['chkur@mailinator.com',], + 'MANAGERS_EMAILS': ['chkur@mailinator.com',], + + 'NEW_REQ_AVAIL_EMAIL_SUBJ': u'Вопрос', + 'DONE_REQ_AVAIL_EMAIL_SUBJ': u'Сообщение обработано менеджером', +} + CELERYBEAT_SCHEDULE = { 'check-license': { 'task': 'project.customer.tasks.check_license', diff --git a/project/static/css/style.css b/project/static/css/style.css index 731489a..6475f85 100644 --- a/project/static/css/style.css +++ b/project/static/css/style.css @@ -222,3 +222,5 @@ div.blockMsg { width: 100%; height: 100%; top: 0; left: 0; text-align: center; } tr.doc-row:hover {cursor:pointer;} #clients tr:nth-child(odd) {background: #f5f5f5;} + +#req-avail-form {display:none;} diff --git a/project/static/js/callback.js b/project/static/js/callback.js new file mode 100644 index 0000000..564f6c8 --- /dev/null +++ b/project/static/js/callback.js @@ -0,0 +1,22 @@ +$(document).ready(function() { + var form = $('#req-avail-form'); + + form.dialog({ + dialogClass: "white-dlg", + modal: true, + autoOpen: false, + Width: 324, + resizable: false, + beforeClose: function(event, ui) { + clear_form_errors(form); + } + }); +}) + +function show_req_avail_form(action) { + var form = $('#req-avail-form'); + form.attr('action', action); + form.dialog(); + form.dialog('open'); + return false; +} diff --git a/project/static/js/commons.js b/project/static/js/commons.js index 4781a52..2cd5571 100644 --- a/project/static/js/commons.js +++ b/project/static/js/commons.js @@ -30,3 +30,4 @@ function getUrlVars() { } return vars; } + diff --git a/project/static/js/dialogs.js b/project/static/js/dialogs.js index 8f9c2ff..0845795 100644 --- a/project/static/js/dialogs.js +++ b/project/static/js/dialogs.js @@ -8,11 +8,11 @@ $(document).ready(function() { dataType: 'json', timeout: 30000, beforeSubmit: function() { - $('input', form).attr('disabled', 'disabled'); + $(':input', form).attr('disabled', 'disabled'); $('button', form).attr('disabled', 'disabled'); }, complete: function(data) { - $('input', form).removeAttr('disabled'); + $(':input', form).removeAttr('disabled'); $('button', form).removeAttr('disabled'); }, diff --git a/project/templates/admin/callback/callback_change.html b/project/templates/admin/callback/callback_change.html new file mode 100644 index 0000000..2056161 --- /dev/null +++ b/project/templates/admin/callback/callback_change.html @@ -0,0 +1,6 @@ +{% extends "admin/change_form.html" %} + +{% block field_sets %} +{% if original.user %}

{{ original.user.profile }}

{% endif %} +{{ block.super }} +{% endblock %} diff --git a/project/templates/base.html b/project/templates/base.html index 5542c41..f724017 100644 --- a/project/templates/base.html +++ b/project/templates/base.html @@ -25,14 +25,17 @@
Документор +
+
{% static_placeholder "header_phone" %} {% static_placeholder "header_address" %} + Оставить сообщение
+
{% if user.is_authenticated %}
@@ -47,6 +50,7 @@ Вход
{% endif %} +
@@ -59,8 +63,10 @@
{% block dialogs %}{% endblock %} +{% include "callback/callback_form.html" %}
+
{% block common_js %} @@ -77,6 +83,7 @@ + {% include 'autocomplete_light/static.html' %} {% endblock %} diff --git a/project/templates/callback/callback_btn.html b/project/templates/callback/callback_btn.html new file mode 100644 index 0000000..c0c55e2 --- /dev/null +++ b/project/templates/callback/callback_btn.html @@ -0,0 +1 @@ +Заказать обратный звонок diff --git a/project/templates/callback/callback_form.html b/project/templates/callback/callback_form.html new file mode 100644 index 0000000..1d745f4 --- /dev/null +++ b/project/templates/callback/callback_form.html @@ -0,0 +1,16 @@ +
+ {% csrf_token %} +
+
Оставьте свои контакты и мы перезвоним вам, ответим на все ваши вопросы
+

{{ reqf.name }}

+

{{ reqf.phone }}

+

{{ reqf.message }}

+

{{ reqf.captcha }}

+
+
+
+
+ +
+
+
diff --git a/project/templates/callback/req_avail_done.txt b/project/templates/callback/req_avail_done.txt new file mode 100755 index 0000000..f36f851 --- /dev/null +++ b/project/templates/callback/req_avail_done.txt @@ -0,0 +1,4 @@ +{% load url from future %} +Сообщение обработано + +Ссылка на сообщение: http://dokumentor.ru{% url 'admin:callback_reqavail_change' req.pk %} diff --git a/project/templates/callback/req_avail_new.txt b/project/templates/callback/req_avail_new.txt new file mode 100644 index 0000000..15c26d2 --- /dev/null +++ b/project/templates/callback/req_avail_new.txt @@ -0,0 +1,14 @@ +{% load url from future %} +Заказ товара с сайта + +Имя клиента: {{ req.name }} +Телефон/E-mail: {{ req.phone }} + +{% if req.good %} +Название: {{ req.good }} +{% else %} +По общим вопросам +{{ req.message }} +{% endif %} + +Ссылка на эту заявку: http://dokumentor.ru{% url "admin:callback_reqavail_change" req.pk %} diff --git a/project/templates/customer/index.html b/project/templates/customer/index.html index 7e0e597..1576ca2 100644 --- a/project/templates/customer/index.html +++ b/project/templates/customer/index.html @@ -15,7 +15,6 @@

Документы

-
{% include "docs/docs.html" %}
{% endblock %} diff --git a/project/templates/docs/_base/preview.html b/project/templates/docs/_base/preview.html index 2f8c578..b2958ff 100644 --- a/project/templates/docs/_base/preview.html +++ b/project/templates/docs/_base/preview.html @@ -23,7 +23,7 @@
{% include "docs/parts/doc_actions.html" %} {% block panel_copy %} - {% if template_create_docs%} + {% if template_create_docs %} {% include template_create_docs %} {% endif %} {% endblock %} diff --git a/project/templates/docs/parts/invoice_create_docs.html b/project/templates/docs/parts/invoice_create_docs.html index 22e5766..e28fe01 100644 --- a/project/templates/docs/parts/invoice_create_docs.html +++ b/project/templates/docs/parts/invoice_create_docs.html @@ -1,8 +1,25 @@ {% url 'docs_aktrabot_add_by_invoice' invoice_id=obj.pk as url_aktrabot_by_invoice %} {% url 'docs_nakladn_add_by_invoice' invoice_id=obj.pk as url_nakladn_by_invoice %} {% url 'docs_faktura_add_by_invoice' invoice_id=obj.pk as url_faktura_by_invoice %} -Акт по счёту
+Акт по счёту
+{% if obj.docs_for_invoice.AktRabot %} +{% for akt in obj.docs_for_invoice.AktRabot %} +{{ akt }} +{% endfor %} +{% endif %}
-Накладную

-Счёт-фактуру
+Накладную
+{% if obj.docs_for_invoice.Nakladn %} +{% for nakladn in obj.docs_for_invoice.Nakladn %} +{{ nakladn }} +{% endfor %} +{% endif %} +
+
+Счёт-фактуру
+{% if obj.docs_for_invoice.Faktura %} +{% for faktura in obj.docs_for_invoice.Faktura %} +{{ faktura }} +{% endfor %} +{% endif %} diff --git a/project/urls.py b/project/urls.py index 80f1a81..ab68330 100644 --- a/project/urls.py +++ b/project/urls.py @@ -25,6 +25,7 @@ urlpatterns = patterns('', url(r'^my/', include('project.customer.urls')), url(r'^my/docs/', include('project.docs.urls')), + url(r'^my/callback/', include('project.callback.urls')), url(r'^user/', include('project.myauth.urls')), url(r'^', include('cms.urls')),