forked from extern/django-helpdesk
Merge 0.2.7 bugfixes
This commit is contained in:
commit
54a6b1d21b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
*.pyc
|
||||
.eggs/
|
||||
/dist/
|
||||
django_helpdesk.egg-info
|
||||
demo/*.egg-info
|
||||
|
@ -7,7 +7,7 @@ python:
|
||||
- "3.6"
|
||||
|
||||
env:
|
||||
- DJANGO=1.11.8
|
||||
- DJANGO=1.11.9
|
||||
|
||||
install:
|
||||
- pip install -q Django==$DJANGO
|
||||
|
3
LICENSE
3
LICENSE
@ -1,4 +1,5 @@
|
||||
Copyright (c) 2008, Ross Poulton (Trading as Jutda)
|
||||
Copyright (c) 2008 Ross Poulton (Trading as Jutda),
|
||||
Copyright (c) 2008-2018 django-helpdesk contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
|
@ -7,7 +7,7 @@ django-helpdesk - A Django powered ticket tracker for small businesses.
|
||||
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
|
||||
|
||||
Copyright 2009-2017 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
|
||||
Copyright 2009-2018 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
|
||||
See LICENSE for details.
|
||||
|
||||
django-helpdesk was formerly known as Jutda Helpdesk, named after the
|
||||
@ -62,7 +62,7 @@ Installation
|
||||
|
||||
`django-helpdesk` requires:
|
||||
|
||||
* Django 1.11.x *only*
|
||||
* Django 1.11.x
|
||||
* either Python 2.7 or 3.4+
|
||||
|
||||
**NOTE REGARDING PYTHON VERSION:**
|
||||
@ -72,6 +72,11 @@ and Django itself (Django 2.0), so users and developers are encouraged to begin
|
||||
transitioning to Python 3 if have not already. New projects should definitely
|
||||
use Python 3!
|
||||
|
||||
**NOTE REGARDING DJANGO VERSION:**
|
||||
The recommended release is Django 1.11. However, there initial support of
|
||||
Django 2.0 as of version 0.2.7 if you'd like to try it out.
|
||||
Please report any bugs you find!
|
||||
|
||||
You can quickly install the latest stable version of `django-helpdesk`
|
||||
app via `pip`::
|
||||
|
||||
|
@ -23,7 +23,8 @@ CLASSIFIERS = ['Development Status :: 4 - Beta',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Framework :: Django']
|
||||
'Framework :: Django :: 1.11',
|
||||
'Framework :: Django :: 2.0']
|
||||
KEYWORDS = []
|
||||
PACKAGES = ['demodesk']
|
||||
REQUIREMENTS = [
|
||||
|
@ -71,5 +71,5 @@ Dependencies
|
||||
|
||||
1. Python 3.4+ (or 2.7, but deprecated and support will be removed next release)
|
||||
2. Django 1.11 or newer
|
||||
3. An existing **working** Django project with database etc. If you cannot log into the Admin, you won't get this product working! This means you **must** run `syncdb` **before** you add ``helpdesk`` to your ``INSTALLED_APPS``.
|
||||
3. An existing **working** Django project with database etc. If you cannot log into the Admin, you won't get this product working! This means you **must** run `migrate` **before** you add ``helpdesk`` to your ``INSTALLED_APPS``.
|
||||
|
||||
|
@ -105,6 +105,22 @@ These options only change display of items on public-facing pages, not staff pag
|
||||
**Default:** ``HELPDESK_SUBMIT_A_TICKET_PUBLIC = True``
|
||||
|
||||
|
||||
Options for public ticket submission form
|
||||
-----------------------------------------
|
||||
|
||||
- **HELPDESK_PUBLIC_TICKET_QUEUE** Sets the queue for tickets submitted through the public form. If defined, the matching form field will be hidden. This cannot be `None` but must be set to a valid queue slug.
|
||||
|
||||
**Default:** Not defined
|
||||
|
||||
- **HELPDESK_PUBLIC_TICKET_PRIORITY** Sets the priority for tickets submitted through the public form. If defined, the matching form field will be hidden. Must be set to a valid integer priority.
|
||||
|
||||
**Default:** Not defined
|
||||
|
||||
- **HELPDESK_PUBLIC_TICKET_DUE_DATE** Sets the due date for tickets submitted through the public form. If defined, the matching form field will be hidden. Set to `None` if you want to hide the form field but do not want to define a value.
|
||||
|
||||
**Default:** Not defined
|
||||
|
||||
|
||||
Options that change ticket updates
|
||||
----------------------------------
|
||||
|
||||
@ -149,9 +165,9 @@ Staff Ticket Creation Settings
|
||||
Staff Ticket View Settings
|
||||
------------------------------
|
||||
|
||||
- **HELPDESK_ENABLE_PER_QUEUE_PERMISSION** If ``True``, logged in staff users only see queues and tickets to which they have specifically been granted access - this holds for the dashboard, ticket query, and ticket report views. User assignment is done through the standard ``django.admin.admin`` permissions. *Note*: Staff with access to admin interface will be able to see the full list of tickets, but won't have access to details and could not modify them. This setting does not prevent staff users from creating tickets for all queues. Also, superuser accounts have full access to all queues, regardless of whatever queue memberships they have been granted.
|
||||
- **HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION** If ``True``, logged in staff users only see queues and tickets to which they have specifically been granted access - this holds for the dashboard, ticket query, and ticket report views. User assignment is done through the standard ``django.admin.admin`` permissions. *Note*: Staff with access to admin interface will be able to see the full list of tickets, but won't have access to details and could not modify them. This setting does not prevent staff users from creating tickets for all queues. Also, superuser accounts have full access to all queues, regardless of whatever queue memberships they have been granted.
|
||||
|
||||
**Default:** ``HELPDESK_ENABLE_PER_QUEUE_PERMISSION = False``
|
||||
**Default:** ``HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = False``
|
||||
|
||||
|
||||
|
||||
|
@ -368,6 +368,14 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
Add any (non-staff) custom fields that are defined to the form
|
||||
"""
|
||||
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
||||
self.fields['queue'].widget = forms.HiddenInput()
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
||||
self.fields['priority'].widget = forms.HiddenInput()
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
||||
self.fields['due_date'].widget = forms.HiddenInput()
|
||||
|
||||
self._add_form_custom_fields(False)
|
||||
|
||||
def save(self):
|
||||
|
@ -176,7 +176,14 @@ def process_queue(q, logger):
|
||||
msgNum = msg.split(" ")[0]
|
||||
logger.info("Processing message %s" % msgNum)
|
||||
|
||||
full_message = encoding.force_text("\n".join(server.retr(msgNum)[1]), errors='replace')
|
||||
if six.PY2:
|
||||
full_message = encoding.force_text("\n".join(server.retr(msgNum)[1]), errors='replace')
|
||||
else:
|
||||
raw_content = server.retr(msgNum)[1]
|
||||
if type(raw_content[0]) is bytes:
|
||||
full_message = "\n".join([elm.decode('utf-8') for elm in raw_content])
|
||||
else:
|
||||
full_message = encoding.force_text("\n".join(raw_content), errors='replace')
|
||||
ticket = ticket_from_message(message=full_message, queue=q, logger=logger)
|
||||
|
||||
if ticket:
|
||||
@ -229,7 +236,10 @@ def process_queue(q, logger):
|
||||
logger.info("Processing message %s" % num)
|
||||
status, data = server.fetch(num, '(RFC822)')
|
||||
full_message = encoding.force_text(data[0][1], errors='replace')
|
||||
ticket = ticket_from_message(message=full_message, queue=q, logger=logger)
|
||||
try:
|
||||
ticket = ticket_from_message(message=full_message, queue=q, logger=logger)
|
||||
except TypeError:
|
||||
ticket = None # hotfix. Need to work out WHY.
|
||||
if ticket:
|
||||
server.store(num, '+FLAGS', '\\Deleted')
|
||||
logger.info("Successfully processed message %s, deleted from IMAP server" % num)
|
||||
@ -389,8 +399,9 @@ def ticket_from_message(message, queue, logger):
|
||||
if not body:
|
||||
mail = BeautifulSoup(part.get_payload(), "lxml")
|
||||
if ">" in mail.text:
|
||||
message_body = mail.text.split(">")[1]
|
||||
body = message_body.encode('ascii', errors='ignore')
|
||||
body = mail.find('body')
|
||||
body = body.text
|
||||
body = body.encode('ascii', errors='ignore')
|
||||
else:
|
||||
body = mail.text
|
||||
|
||||
@ -459,7 +470,7 @@ def ticket_from_message(message, queue, logger):
|
||||
for ccemail in new_cc:
|
||||
tcc = TicketCC.objects.create(
|
||||
ticket=t,
|
||||
email=ccemail,
|
||||
email=ccemail.replace('\n', ' ').replace('\r', ' '),
|
||||
can_view=True,
|
||||
can_update=False
|
||||
)
|
||||
|
23
helpdesk/migrations/0017_default_owner_on_delete_null.py
Normal file
23
helpdesk/migrations/0017_default_owner_on_delete_null.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2018-01-19 09:48
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0016_alter_model_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='default_owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
|
||||
),
|
||||
]
|
@ -14,6 +14,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
@ -1120,7 +1121,10 @@ class UserSettings(models.Model):
|
||||
except ImportError:
|
||||
import cPickle as pickle
|
||||
from helpdesk.lib import b64encode
|
||||
self.settings_pickled = b64encode(pickle.dumps(data))
|
||||
if six.PY2:
|
||||
self.settings_pickled = b64encode(pickle.dumps(data))
|
||||
else:
|
||||
self.settings_pickled = b64encode(pickle.dumps(data)).decode()
|
||||
|
||||
def _get_settings(self):
|
||||
# return a python dictionary representing the pickled data.
|
||||
@ -1130,13 +1134,10 @@ class UserSettings(models.Model):
|
||||
import cPickle as pickle
|
||||
from helpdesk.lib import b64decode
|
||||
try:
|
||||
if six.PY3:
|
||||
if type(self.settings_pickled) is bytes:
|
||||
return pickle.loads(b64decode(str(self.settings_pickled, 'utf8')))
|
||||
else:
|
||||
return pickle.loads(b64decode(bytes(self.settings_pickled, 'utf8')))
|
||||
else:
|
||||
if six.PY2:
|
||||
return pickle.loads(b64decode(str(self.settings_pickled)))
|
||||
else:
|
||||
return pickle.loads(b64decode(self.settings_pickled.encode('utf-8')))
|
||||
except pickle.UnpicklingError:
|
||||
return {}
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
*
|
||||
* http://api.jqueryui.com/category/theming/
|
||||
*
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6
|
||||
*/
|
||||
|
||||
|
||||
|
@ -6,4 +6,4 @@
|
||||
|
||||
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'><b>{{ queue.title }}</b>{% if queue.email_address %}<br><a href='mailto:{{ queue.email_address }}'>{{ queue.email_address }}</a>{% endif %}</p>
|
||||
|
||||
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 9pt; color: #808080;' color='#808080'>This e-mail was sent to you as a user of our support service, in accordance with our privacy policy. Please advise us if you believe you have received this e-mail in error.</p>
|
||||
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 9pt; color: #808080;'>This e-mail was sent to you as a user of our support service, in accordance with our privacy policy. Please advise us if you believe you have received this e-mail in error.</p>
|
||||
|
@ -105,6 +105,9 @@ $(document).on('change', ':file', function() {
|
||||
</li>
|
||||
{% if forloop.last %}</ul></div>{% endif %}
|
||||
{% endfor %}
|
||||
<!--- ugly long test to suppress the following if it will be empty, to save vertical space -->
|
||||
{% with possible=helpdesk_settings.HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP %}
|
||||
{% if possible and followup.user and request.user == followup.user and not followup.ticketchange_set.all or possible and user.is_superuser and helpdesk_settings.HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP %}
|
||||
<hr>
|
||||
<div class="btn-group">
|
||||
{% if helpdesk_settings.HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP %}
|
||||
@ -116,6 +119,7 @@ $(document).on('change', ':file', function() {
|
||||
<a href="{% url 'helpdesk:followup_delete' ticket.id followup.id %}" class='followup-edit'><button type="button" class="btn btn-warning btn-xs"><i class="fa fa-trash"></i> {% trans "Delete" %}</button></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import mail
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
@ -11,6 +12,7 @@ except ImportError: # python 2
|
||||
from urlparse import urlparse
|
||||
|
||||
from helpdesk.templatetags.ticket_to_link import num_to_link
|
||||
from helpdesk.views.staff import _is_my_ticket
|
||||
|
||||
|
||||
class TicketActionsTestCase(TestCase):
|
||||
@ -22,7 +24,8 @@ class TicketActionsTestCase(TestCase):
|
||||
slug='q1',
|
||||
allow_public_submission=True,
|
||||
new_ticket_cc='new.public@example.com',
|
||||
updated_ticket_cc='update.public@example.com')
|
||||
updated_ticket_cc='update.public@example.com'
|
||||
)
|
||||
|
||||
self.ticket_data = {
|
||||
'title': 'Test Ticket',
|
||||
@ -32,6 +35,7 @@ class TicketActionsTestCase(TestCase):
|
||||
self.client = Client()
|
||||
|
||||
def loginUser(self, is_staff=True):
|
||||
"""Create a staff user and login"""
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create(
|
||||
username='User_1',
|
||||
@ -124,6 +128,32 @@ class TicketActionsTestCase(TestCase):
|
||||
response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True)
|
||||
self.assertContains(response, 'Changed Status from Open to Closed')
|
||||
|
||||
def test_is_my_ticket(self):
|
||||
"""Tests whether non-staff but assigned user still counts as owner"""
|
||||
|
||||
# make non-staff user
|
||||
self.loginUser(is_staff=False)
|
||||
|
||||
# create second user
|
||||
User = get_user_model()
|
||||
self.user2 = User.objects.create(
|
||||
username='User_2',
|
||||
is_staff=False,
|
||||
)
|
||||
|
||||
initial_data = {
|
||||
'title': 'Private ticket test',
|
||||
'queue': self.queue_public,
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
}
|
||||
|
||||
# create ticket
|
||||
ticket = Ticket.objects.create(**initial_data)
|
||||
|
||||
self.assertEqual(_is_my_ticket(self.user, ticket), True)
|
||||
self.assertEqual(_is_my_ticket(self.user2, ticket), False)
|
||||
|
||||
def test_num_to_link(self):
|
||||
"""Test that we are correctly expanding links to tickets from IDs"""
|
||||
|
||||
|
@ -7,11 +7,17 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
try:
|
||||
# Django 2.0+
|
||||
from django.urls import reverse
|
||||
except ImportError:
|
||||
# Django < 2
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.http import urlquote
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
||||
@ -61,6 +67,19 @@ def homepage(request):
|
||||
except Queue.DoesNotExist:
|
||||
queue = None
|
||||
initial_data = {}
|
||||
|
||||
# add pre-defined data for public ticket
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
||||
# get the requested queue; return an error if queue not found
|
||||
try:
|
||||
queue = Queue.objects.get(slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE)
|
||||
except Queue.DoesNotExist:
|
||||
return HttpResponse(status=500)
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
||||
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
||||
initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
|
||||
|
||||
if queue:
|
||||
initial_data['queue'] = queue.id
|
||||
|
||||
@ -91,6 +110,8 @@ def view_ticket(request):
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
except ObjectDoesNotExist:
|
||||
error_message = _('Invalid ticket ID or e-mail address. Please try again.')
|
||||
except ValueError:
|
||||
error_message = _('Invalid ticket ID or e-mail address. Please try again.')
|
||||
else:
|
||||
if is_helpdesk_staff(request.user):
|
||||
redirect_url = reverse('helpdesk:view', args=[ticket_id])
|
||||
|
@ -92,7 +92,7 @@ def _has_access_to_queue(user, queue):
|
||||
def _is_my_ticket(user, ticket):
|
||||
"""Check to see if the user has permission to access
|
||||
a ticket. If not then deny access."""
|
||||
if user.is_superuser or user.is_staff or user.id == ticket.customer_id:
|
||||
if user.is_superuser or user.is_staff or user.id == ticket.assigned_to.id:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django>=1.11,<2
|
||||
Django>=1.11,<3
|
||||
django-bootstrap-form>=3.3,<4
|
||||
email-reply-parser
|
||||
django-markdown-deux
|
||||
|
Loading…
Reference in New Issue
Block a user