Hendrik Sünkler 4 years ago
commit
3e8346372f
91 changed files with 16528 additions and 0 deletions
  1. 25
    0
      .gitignore
  2. 21
    0
      LICENSE
  3. 72
    0
      README.md
  4. 0
    0
      main/__init__.py
  5. 17
    0
      main/admin.py
  6. 38
    0
      main/forms.py
  7. 0
    0
      main/management/__init__.py
  8. 0
    0
      main/management/commands/__init__.py
  9. 303
    0
      main/management/commands/get_email.py
  10. 79
    0
      main/migrations/0001_initial.py
  11. 0
    0
      main/migrations/__init__.py
  12. 100
    0
      main/models.py
  13. 68
    0
      main/templates/main/all-tickets.html
  14. 56
    0
      main/templates/main/archive.html
  15. 22
    0
      main/templates/main/attachment_add.html
  16. 112
    0
      main/templates/main/base.html
  17. 33
    0
      main/templates/main/followup_edit.html
  18. 55
    0
      main/templates/main/inbox.html
  19. 117
    0
      main/templates/main/my-tickets.html
  20. 32
    0
      main/templates/main/settings.html
  21. 113
    0
      main/templates/main/ticket_detail.html
  22. 33
    0
      main/templates/main/ticket_edit.html
  23. 190
    0
      main/templates/registration/login.html
  24. 3
    0
      main/tests.py
  25. 222
    0
      main/views.py
  26. 10
    0
      manage.py
  27. 5
    0
      requirements.txt
  28. BIN
      screenshots/screenshot_all_open_tickets.png
  29. BIN
      screenshots/screenshot_archive.png
  30. BIN
      screenshots/screenshot_detail_view.png
  31. BIN
      screenshots/screenshot_inbox.png
  32. BIN
      screenshots/screenshot_landing_page.png
  33. BIN
      screenshots/screenshot_login.png
  34. BIN
      screenshots/screenshot_my_tickets.png
  35. 61
    0
      static/500.html
  36. 457
    0
      static/bootstrap/css/bootstrap-theme.css
  37. 1
    0
      static/bootstrap/css/bootstrap-theme.css.map
  38. 5
    0
      static/bootstrap/css/bootstrap-theme.min.css
  39. 6358
    0
      static/bootstrap/css/bootstrap.css
  40. 1
    0
      static/bootstrap/css/bootstrap.css.map
  41. 5
    0
      static/bootstrap/css/bootstrap.min.css
  42. BIN
      static/bootstrap/fonts/glyphicons-halflings-regular.eot
  43. 229
    0
      static/bootstrap/fonts/glyphicons-halflings-regular.svg
  44. BIN
      static/bootstrap/fonts/glyphicons-halflings-regular.ttf
  45. BIN
      static/bootstrap/fonts/glyphicons-halflings-regular.woff
  46. 2276
    0
      static/bootstrap/js/bootstrap.js
  47. 7
    0
      static/bootstrap/js/bootstrap.min.js
  48. 13
    0
      static/bootstrap/js/npm.js
  49. 93
    0
      static/css/frontpage.css
  50. 10
    0
      static/css/print.css
  51. 73
    0
      static/css/style.css
  52. 1672
    0
      static/font-awesome/css/font-awesome.css
  53. 4
    0
      static/font-awesome/css/font-awesome.min.css
  54. BIN
      static/font-awesome/fonts/FontAwesome.otf
  55. BIN
      static/font-awesome/fonts/fontawesome-webfont.eot
  56. 520
    0
      static/font-awesome/fonts/fontawesome-webfont.svg
  57. BIN
      static/font-awesome/fonts/fontawesome-webfont.ttf
  58. BIN
      static/font-awesome/fonts/fontawesome-webfont.woff
  59. 16
    0
      static/font-awesome/less/bordered-pulled.less
  60. 11
    0
      static/font-awesome/less/core.less
  61. 6
    0
      static/font-awesome/less/fixed-width.less
  62. 17
    0
      static/font-awesome/less/font-awesome.less
  63. 552
    0
      static/font-awesome/less/icons.less
  64. 13
    0
      static/font-awesome/less/larger.less
  65. 19
    0
      static/font-awesome/less/list.less
  66. 25
    0
      static/font-awesome/less/mixins.less
  67. 14
    0
      static/font-awesome/less/path.less
  68. 20
    0
      static/font-awesome/less/rotated-flipped.less
  69. 29
    0
      static/font-awesome/less/spinning.less
  70. 20
    0
      static/font-awesome/less/stacked.less
  71. 561
    0
      static/font-awesome/less/variables.less
  72. 16
    0
      static/font-awesome/scss/_bordered-pulled.scss
  73. 11
    0
      static/font-awesome/scss/_core.scss
  74. 6
    0
      static/font-awesome/scss/_fixed-width.scss
  75. 552
    0
      static/font-awesome/scss/_icons.scss
  76. 13
    0
      static/font-awesome/scss/_larger.scss
  77. 19
    0
      static/font-awesome/scss/_list.scss
  78. 25
    0
      static/font-awesome/scss/_mixins.scss
  79. 14
    0
      static/font-awesome/scss/_path.scss
  80. 20
    0
      static/font-awesome/scss/_rotated-flipped.scss
  81. 29
    0
      static/font-awesome/scss/_spinning.scss
  82. 20
    0
      static/font-awesome/scss/_stacked.scss
  83. 561
    0
      static/font-awesome/scss/_variables.scss
  84. 17
    0
      static/font-awesome/scss/font-awesome.scss
  85. BIN
      static/img/frontpage/mt-fuji-477832_1280.jpg
  86. 4
    0
      static/jquery/jquery-1.11.1.min.js
  87. 196
    0
      static/js/jquery.hotkeys.js
  88. 0
    0
      tickets/__init__.py
  89. 151
    0
      tickets/settings.py
  90. 46
    0
      tickets/urls.py
  91. 14
    0
      tickets/wsgi.py

+ 25
- 0
.gitignore View File

@@ -0,0 +1,25 @@
1
+# compiled python files
2
+*.py[cod]
3
+ 
4
+# emacs temp files
5
+*~
6
+[#]*[#]
7
+.\#*
8
+
9
+# pycharm settings
10
+.idea/
11
+
12
+# sphinx build folder
13
+doc/_build
14
+
15
+# OS generated files
16
+.DS_Store?
17
+
18
+# no media files in repo
19
+media/
20
+
21
+# database not in repo
22
+db.sqlite3
23
+
24
+# bash script to set environment variables
25
+env.sh

+ 21
- 0
LICENSE View File

@@ -0,0 +1,21 @@
1
+The MIT License (MIT)
2
+
3
+Copyright (c) 2014 Hendrik Sünkler
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 72
- 0
README.md View File

@@ -0,0 +1,72 @@
1
+A simple ticketing application
2
+==============================
3
+
4
+`django-tickets` is a simple MIT-licensed ticketing application written in Python/Django. Some of the features are:
5
+
6
+- creation of new tickets via web interface or via email
7
+- followups on tickets
8
+- file attachments for tickets
9
+- assign tickets to users
10
+- email notifications for new assignments, followups and closed tickets
11
+
12
+The application was written to serve my special needs.  It is not intended to grow up to a kitchen sink.  But I will add some features in the future.  Feel free to use and modify it, if it is interesting for you.
13
+
14
+What does it look like?
15
+=======================
16
+
17
+`django-tickets` uses a simple Bootstrap template. Nothing fancy.
18
+
19
+![Landing Page](screenshots/screenshot_landing_page.png?raw=true "Landing Page")
20
+![My tickets](screenshots/screenshot_my_tickets.png?raw=true "My tickets")
21
+
22
+Installation
23
+============
24
+
25
+Sensitive and installation dependent information is expected in environment variables. You can use a bash script like this one:
26
+
27
+```
28
+#!/usr/bin/env bash
29
+
30
+export DJANGO_SECRET_KEY="xxx"
31
+export DJANGO_PRODUCTION_DOMAIN="xxx"
32
+
33
+# static and media files dir in production
34
+export DJANGO_STATIC_ROOT="xxx"
35
+export DJANGO_MEDIA_ROOT="xxx"
36
+
37
+# User who gets django's email notifications (ADMINS/MANAGERS), see settings.py
38
+export DJANGO_ADMIN_NAME="xxx"
39
+export DJANGO_ADMIN_EMAIL="xxx"
40
+
41
+# Django email configuration
42
+export DJANGO_EMAIL_HOST="xxx"
43
+export DJANGO_EMAIL_HOST_USER="xxx"
44
+export DJANGO_EMAIL_HOST_PASSWORD="xxx"
45
+
46
+# ticket email inbox, see 'main/management/commands/get_email.py'
47
+export DJANGO_TICKET_INBOX_SERVER="xxx"
48
+export DJANGO_TICKET_INBOX_USER="xxx"
49
+export DJANGO_TICKET_INBOX_PASSWORD="xxx"
50
+
51
+# email notifications to admin, see 'main/management/commands/get_email.py'
52
+export DJANGO_TICKET_EMAIL_NOTIFICATIONS_FROM="xxx"
53
+export DJANGO_TICKET_EMAIL_NOTIFICATIONS_TO="xxx"
54
+```
55
+
56
+Please note that `django-tickets` is **not** packaged as a reusable django app; it's a **complete django project**. So just clone the repository and install the dependencies via pip and the application including user authentication is ready to go. 
57
+
58
+```
59
+$ git clone https://github.com/suenkler/django-tickets.git
60
+$ cd django-tickets
61
+$ pip install -r requirements.txt
62
+$ source env.sh
63
+$ ./manage.py migrate
64
+$ ./manage.py createsuperuser
65
+$ ./manage.py runserver
66
+```
67
+
68
+To check the IMAP account for new messages and create tickets out of these messages, use the management command `get_email`:
69
+
70
+```
71
+$ ./manage.py get_email
72
+```

+ 0
- 0
main/__init__.py View File


+ 17
- 0
main/admin.py View File

@@ -0,0 +1,17 @@
1
+from django.contrib import admin
2
+from .models import Ticket, FollowUp, Attachment
3
+
4
+
5
+class TicketAdmin(admin.ModelAdmin):
6
+    list_display = ('id',
7
+                    'title',
8
+                    'description',
9
+                    'assigned_to',
10
+                    'created',
11
+                    'updated',)
12
+
13
+
14
+# Register Models
15
+admin.site.register(Ticket, TicketAdmin)
16
+admin.site.register(FollowUp)
17
+admin.site.register(Attachment)

+ 38
- 0
main/forms.py View File

@@ -0,0 +1,38 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django import forms
4
+from django.contrib.auth.models import User
5
+
6
+from .models import Ticket, FollowUp, Attachment
7
+
8
+
9
+class UserSettingsForm(forms.ModelForm):
10
+
11
+    class Meta:
12
+        model = User
13
+        fields = ('first_name', 'last_name', 'email',)
14
+
15
+
16
+class TicketCreateForm(forms.ModelForm):
17
+    class Meta:
18
+        model = Ticket
19
+        fields = ('title', 'description')
20
+
21
+        
22
+class TicketEditForm(forms.ModelForm):
23
+    class Meta:
24
+        model = Ticket
25
+        fields = ('title', 'owner', 'description', 'status', 'waiting_for', 'assigned_to')
26
+
27
+
28
+class FollowupForm(forms.ModelForm):
29
+
30
+    class Meta:
31
+        model = FollowUp
32
+        fields = ('ticket', 'title', 'text', 'user')
33
+
34
+
35
+class AttachmentForm(forms.ModelForm):
36
+    class Meta:
37
+        model = Attachment
38
+        fields = ('file',)

+ 0
- 0
main/management/__init__.py View File


+ 0
- 0
main/management/commands/__init__.py View File


+ 303
- 0
main/management/commands/get_email.py View File

@@ -0,0 +1,303 @@
1
+"""
2
+This file was derived from:
3
+https://github.com/rossp/django-helpdesk/blob/master/helpdesk/management/commands/get_email.py
4
+
5
+Copyright notice for that original file:
6
+
7
+Copyright (c) 2008, Ross Poulton (Trading as Jutda)
8
+All rights reserved.
9
+
10
+Redistribution and use in source and binary forms, with or without modification,
11
+are permitted provided that the following conditions are met:
12
+
13
+    1. Redistributions of source code must retain the above copyright
14
+       notice, this list of conditions and the following disclaimer.
15
+
16
+    2. Redistributions in binary form must reproduce the above copyright
17
+       notice, this list of conditions and the following disclaimer in the
18
+       documentation and/or other materials provided with the distribution.
19
+
20
+    3. Neither the name of Ross Poulton, Jutda, nor the names of any
21
+       of its contributors may be used to endorse or promote products
22
+       derived from this software without specific prior written permission.
23
+
24
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+"""
35
+
36
+import email
37
+import imaplib
38
+import mimetypes
39
+import re
40
+import os
41
+from email.header import decode_header
42
+from email.utils import parseaddr, collapse_rfc2231_value
43
+from optparse import make_option
44
+from email_reply_parser import EmailReplyParser
45
+from django.core.files.base import ContentFile
46
+from django.core.management.base import BaseCommand
47
+
48
+from django.contrib.auth.models import User
49
+
50
+try:
51
+    from django.utils import timezone
52
+except ImportError:
53
+    from datetime import datetime as timezone
54
+
55
+from main.models import Ticket, Attachment, FollowUp
56
+
57
+
58
+class Command(BaseCommand):
59
+    def __init__(self):
60
+        BaseCommand.__init__(self)
61
+
62
+        self.option_list += (
63
+            make_option(
64
+                '--quiet', '-q',
65
+                default=False,
66
+                action='store_true',
67
+                help='Hide details about each message as they are processed.'),
68
+            )
69
+
70
+    help = 'Process email inbox and create tickets.'
71
+
72
+    def handle(self, *args, **options):
73
+        quiet = options.get('quiet', False)
74
+        process_inbox(quiet=quiet)
75
+
76
+
77
+def process_inbox(quiet=False):
78
+    """
79
+    Process IMAP inbox
80
+    """
81
+    server = imaplib.IMAP4_SSL(os.environ["DJANGO_TICKET_INBOX_SERVER"], 993)
82
+    server.login(os.environ["DJANGO_TICKET_INBOX_USER"], os.environ["DJANGO_TICKET_INBOX_PASSWORD"])
83
+    server.select("INBOX")
84
+    status, data = server.search(None, 'NOT', 'DELETED')
85
+    if data:
86
+        msgnums = data[0].split()
87
+        for num in msgnums:
88
+            status, data = server.fetch(num, '(RFC822)')
89
+            ticket = ticket_from_message(message=data[0][1], quiet=quiet)
90
+            if ticket:
91
+                server.store(num, '+FLAGS', '\\Deleted')
92
+    server.expunge()
93
+    server.close()
94
+    server.logout()
95
+
96
+
97
+def decodeUnknown(charset, string):
98
+    if not charset:
99
+        try:
100
+            return string.decode('utf-8', 'ignore')
101
+        except:
102
+            return string.decode('iso8859-1', 'ignore')
103
+    return unicode(string, charset)
104
+
105
+
106
+def decode_mail_headers(string):
107
+    decoded = decode_header(string)
108
+    return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded])
109
+
110
+
111
+def ticket_from_message(message, quiet):
112
+    """
113
+    Create a ticket or a followup (if ticket id in subject)
114
+    """
115
+    msg = message
116
+    message = email.message_from_string(msg)
117
+    subject = message.get('subject', 'Created from e-mail')
118
+    subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject))
119
+    sender = message.get('from', ('Unknown Sender'))
120
+    sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
121
+    sender_email = parseaddr(sender)[1]
122
+    body_plain, body_html = '', ''
123
+
124
+    matchobj = re.match(r".*\["+"-(?P<id>\d+)\]", subject)
125
+    if matchobj:
126
+        # This is a reply or forward.
127
+        ticket = matchobj.group('id')
128
+    else:
129
+        ticket = None
130
+
131
+    counter = 0
132
+    files = []
133
+
134
+    for part in message.walk():
135
+        if part.get_content_maintype() == 'multipart':
136
+            continue
137
+
138
+        name = part.get_param("name")
139
+        if name:
140
+            name = collapse_rfc2231_value(name)
141
+
142
+        if part.get_content_maintype() == 'text' and name == None:
143
+            if part.get_content_subtype() == 'plain':
144
+                body_plain = EmailReplyParser.parse_reply(decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)))
145
+            else:
146
+                body_html = part.get_payload(decode=True)
147
+        else:
148
+            if not name:
149
+                ext = mimetypes.guess_extension(part.get_content_type())
150
+                name = "part-%i%s" % (counter, ext)
151
+
152
+            files.append({
153
+                'filename': name,
154
+                'content': part.get_payload(decode=True),
155
+                'type': part.get_content_type()},
156
+            )
157
+
158
+        counter += 1
159
+
160
+    if body_plain:
161
+        body = body_plain
162
+    else:
163
+        body = 'No plain-text email body available. Please see attachment email_html_body.html.'
164
+
165
+    if body_html:
166
+        files.append({
167
+            'filename': 'email_html_body.html',
168
+            'content': body_html,
169
+            'type': 'text/html',
170
+        })
171
+
172
+    now = timezone.now()
173
+
174
+    if ticket:
175
+        try:
176
+            t = Ticket.objects.get(id=ticket)
177
+            new = False
178
+        except Ticket.DoesNotExist:
179
+            ticket = None
180
+
181
+    if ticket == None:
182
+
183
+        # set owner depending on sender_email
184
+        # list of all email addresses from the user model
185
+        users = User.objects.all()
186
+        email_addresses = []
187
+        for user in users:
188
+            email_addresses.append(user.email)
189
+
190
+        ############################################################
191
+        # if ticket id in subject => new followup instead of new ticket
192
+        tickets = Ticket.objects.all()
193
+        ticket_ids = []
194
+        for ticket in tickets:
195
+            ticket_ids.append(ticket.id)
196
+
197
+        # extract id from subject
198
+        subject_id = re.search(r'\[#(\d*)\]\s.*', subject)
199
+        try:
200
+            subject_id = subject_id.group(1)
201
+        except:
202
+            subject_id = "0000"  # no valid id
203
+
204
+        # if there was an ID in the subject, create followup
205
+        if int(subject_id) in ticket_ids:
206
+
207
+            if sender_email in email_addresses:
208
+                f = FollowUp(
209
+                           title=subject,
210
+                           created=now,
211
+                           text=body,
212
+                           ticket=Ticket.objects.get(id=subject_id),
213
+                           user=User.objects.get(email=sender_email),
214
+                )
215
+            else:
216
+                f = FollowUp(
217
+                           title=subject,
218
+                           created=now,
219
+                           text=body,
220
+                           ticket=Ticket.objects.get(id=subject_id),
221
+                )
222
+
223
+            f.save()
224
+
225
+        # if no ID in the subject, create ticket
226
+        else:
227
+
228
+            # if known sender, set also the field owner
229
+            if sender_email in email_addresses:
230
+                t = Ticket(
231
+                           title=subject,
232
+                           status="TODO",
233
+                           created=now,
234
+                           description=body,
235
+                           owner=User.objects.get(email=sender_email),
236
+                )
237
+            # if unknown sender, skip the field owner
238
+            else:
239
+                t = Ticket(
240
+                           title=subject,
241
+                           status="TODO",
242
+                           created=now,
243
+                           description=body,
244
+                )
245
+
246
+            t.save()
247
+
248
+            from django.core.mail import send_mail
249
+            notification_subject = "[#" + str(t.id) + "] New ticket created"
250
+            notification_body = "Hi,\n\na new ticket was created: http://localhost:8000/ticket/" \
251
+                                + str(t.id) + "/"
252
+            send_mail(notification_subject, notification_body, os.environ["DJANGO_TICKET_EMAIL_NOTIFICATIONS_FROM"],
253
+                            [os.environ["DJANGO_TICKET_EMAIL_NOTIFICATIONS_TO"]], fail_silently=False)
254
+
255
+        ############################################################
256
+
257
+        new = True
258
+        update = ''
259
+
260
+    elif t.status == Ticket.CLOSED_STATUS:
261
+        t.status = Ticket.REOPENED_STATUS
262
+        t.save()
263
+
264
+    # files of followups should be assigned to the corresponding ticket
265
+    for file in files:
266
+
267
+        if file['content']:
268
+
269
+            filename = file['filename'].encode('ascii', 'replace').replace(' ', '_')
270
+            filename = re.sub('[^a-zA-Z0-9._-]+', '', filename)
271
+
272
+            # if followup
273
+            if int(subject_id) in ticket_ids:
274
+                a = Attachment(
275
+                           ticket=Ticket.objects.get(id=subject_id),
276
+                           filename=filename,
277
+                           #mime_type=file['type'],
278
+                           #size=len(file['content']),
279
+                )
280
+
281
+            # if new ticket
282
+            else:
283
+                a = Attachment(
284
+                           ticket=t,
285
+                           filename=filename,
286
+                           #mime_type=file['type'],
287
+                           #size=len(file['content']),
288
+                )
289
+
290
+            a.file.save(filename, ContentFile(file['content']), save=False)
291
+            a.save()
292
+
293
+            if not quiet:
294
+                print " - %s" % filename
295
+
296
+    if int(subject_id) in ticket_ids:
297
+        return f
298
+    else:
299
+        return t
300
+
301
+
302
+if __name__ == '__main__':
303
+    process_email()

+ 79
- 0
main/migrations/0001_initial.py View File

@@ -0,0 +1,79 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.db import models, migrations
5
+import main.models
6
+import django.utils.timezone
7
+from django.conf import settings
8
+
9
+
10
+class Migration(migrations.Migration):
11
+
12
+    dependencies = [
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='Attachment',
19
+            fields=[
20
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21
+                ('file', models.FileField(upload_to=main.models.attachment_path, max_length=1000, verbose_name=b'File')),
22
+                ('filename', models.CharField(max_length=1000, verbose_name=b'Filename')),
23
+                ('created', models.DateTimeField(auto_now_add=True)),
24
+            ],
25
+            options={
26
+                'verbose_name': 'Attachment',
27
+                'verbose_name_plural': 'Attachments',
28
+            },
29
+        ),
30
+        migrations.CreateModel(
31
+            name='FollowUp',
32
+            fields=[
33
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
34
+                ('date', models.DateTimeField(default=django.utils.timezone.now, verbose_name=b'Date')),
35
+                ('title', models.CharField(max_length=200, verbose_name=b'Title')),
36
+                ('text', models.TextField(null=True, verbose_name=b'Text', blank=True)),
37
+                ('created', models.DateTimeField(auto_now_add=True)),
38
+                ('modified', models.DateTimeField(auto_now=True)),
39
+            ],
40
+            options={
41
+                'ordering': ['-modified'],
42
+            },
43
+        ),
44
+        migrations.CreateModel(
45
+            name='Ticket',
46
+            fields=[
47
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
48
+                ('title', models.CharField(max_length=255, verbose_name=b'Title')),
49
+                ('description', models.TextField(null=True, verbose_name=b'Description', blank=True)),
50
+                ('status', models.CharField(blank=True, max_length=255, null=True, verbose_name=b'Status', choices=[(b'TODO', b'TODO'), (b'IN PROGRESS', b'IN PROGRESS'), (b'WAITING', b'WAITING'), (b'DONE', b'DONE')])),
51
+                ('closed_date', models.DateTimeField(null=True, blank=True)),
52
+                ('created', models.DateTimeField(auto_now_add=True)),
53
+                ('updated', models.DateTimeField(auto_now=True)),
54
+                ('assigned_to', models.ForeignKey(related_name='assigned_to', verbose_name=b'Assigned to', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
55
+                ('owner', models.ForeignKey(related_name='owner', verbose_name=b'Owner', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
56
+                ('waiting_for', models.ForeignKey(related_name='waiting_for', verbose_name=b'Waiting For', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
57
+            ],
58
+        ),
59
+        migrations.AddField(
60
+            model_name='followup',
61
+            name='ticket',
62
+            field=models.ForeignKey(verbose_name=b'Ticket', to='main.Ticket'),
63
+        ),
64
+        migrations.AddField(
65
+            model_name='followup',
66
+            name='user',
67
+            field=models.ForeignKey(verbose_name=b'User', blank=True, to=settings.AUTH_USER_MODEL, null=True),
68
+        ),
69
+        migrations.AddField(
70
+            model_name='attachment',
71
+            name='ticket',
72
+            field=models.ForeignKey(verbose_name=b'Ticket', to='main.Ticket'),
73
+        ),
74
+        migrations.AddField(
75
+            model_name='attachment',
76
+            name='user',
77
+            field=models.ForeignKey(verbose_name=b'User', blank=True, to=settings.AUTH_USER_MODEL, null=True),
78
+        ),
79
+    ]

+ 0
- 0
main/migrations/__init__.py View File


+ 100
- 0
main/models.py View File

@@ -0,0 +1,100 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.db import models
4
+from django.contrib.auth.models import User
5
+
6
+try:
7
+    from django.utils import timezone
8
+except ImportError:
9
+    from datetime import datetime as timezone
10
+
11
+
12
+def user_unicode(self):
13
+    """
14
+    return 'last_name, first_name' for User by default
15
+    """
16
+    return  u'%s, %s' % (self.last_name, self.first_name)
17
+User.__unicode__ = user_unicode
18
+
19
+
20
+class Ticket(models.Model):
21
+
22
+    title = models.CharField('Title', max_length=255)
23
+    owner = models.ForeignKey(User, related_name='owner', blank=True, null=True, verbose_name='Owner', )
24
+    description = models.TextField('Description', blank=True, null=True)
25
+
26
+    STATUS_CHOICES = (
27
+        ('TODO', 'TODO'),
28
+        ('IN PROGRESS', 'IN PROGRESS'),
29
+        ('WAITING', 'WAITING'),
30
+        ('DONE', 'DONE'),
31
+    )
32
+    status = models.CharField('Status', choices=STATUS_CHOICES, max_length=255, blank=True, null=True)
33
+    waiting_for = models.ForeignKey(User, related_name='waiting_for', blank=True, null=True, verbose_name='Waiting For', )
34
+    closed_date = models.DateTimeField(blank=True, null=True)  # set in view when status changed to "DONE"
35
+
36
+    assigned_to = models.ForeignKey(User,
37
+                                    related_name='assigned_to',
38
+                                    blank=True,
39
+                                    null=True,
40
+                                    verbose_name='Assigned to',)
41
+
42
+    created = models.DateTimeField(auto_now_add=True)
43
+    updated = models.DateTimeField(auto_now=True)
44
+
45
+    def __unicode__(self):
46
+        return str(self.id)
47
+
48
+
49
+class FollowUp(models.Model):
50
+    """
51
+    A FollowUp is a comment to a ticket.
52
+    """
53
+    ticket = models.ForeignKey(Ticket, verbose_name='Ticket', )
54
+    date = models.DateTimeField('Date', default=timezone.now)
55
+    title = models.CharField('Title', max_length=200, )
56
+    text = models.TextField('Text', blank=True, null=True, )
57
+    user = models.ForeignKey(User, blank=True, null=True, verbose_name='User', )
58
+    created = models.DateTimeField(auto_now_add=True)
59
+    modified = models.DateTimeField(auto_now=True)
60
+
61
+    class Meta:
62
+        ordering = ['-modified', ]
63
+
64
+
65
+def attachment_path(instance, filename):
66
+    """
67
+    Provide a file path that will help prevent files being overwritten, by
68
+    putting attachments in a folder off attachments for ticket/followup_id/.
69
+    """
70
+    import os
71
+    from django.conf import settings
72
+    os.umask(0)
73
+    path = 'tickets/%s' % instance.ticket.id
74
+    print(path)
75
+    att_path = os.path.join(settings.MEDIA_ROOT, path)
76
+    if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage":
77
+        if not os.path.exists(att_path):
78
+            os.makedirs(att_path, 0777)
79
+    return os.path.join(path, filename)
80
+
81
+
82
+class Attachment(models.Model):
83
+    ticket = models.ForeignKey(Ticket, verbose_name='Ticket', )
84
+    file = models.FileField('File', upload_to=attachment_path, max_length=1000, )
85
+    filename = models.CharField('Filename', max_length=1000, )
86
+    user = models.ForeignKey(User, blank=True, null=True, verbose_name='User', )
87
+    created = models.DateTimeField(auto_now_add=True)
88
+
89
+    def get_upload_to(self, field_attname):
90
+        """ Get upload_to path specific to this item """
91
+        if not self.id:
92
+            return u''
93
+        return u'../media/tickets/%s' % (
94
+            self.ticket.id,
95
+        )
96
+
97
+    class Meta:
98
+        #ordering = ['filename', ]
99
+        verbose_name = 'Attachment'
100
+        verbose_name_plural = 'Attachments'

+ 68
- 0
main/templates/main/all-tickets.html View File

@@ -0,0 +1,68 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block title %}Tickets - All tickets{% endblock %}
4
+
5
+{% block header_icon %}<i class="fa fa-file-text-o fa-5x"></i>{% endblock %}
6
+{% block headline %}All open Tickets{% endblock %}
7
+{% block head-message %}Overview of all open tickets in the system{% endblock %}
8
+
9
+{% block content %}
10
+<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.css">
11
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/1.10.4/js/jquery.dataTables.min.js"></script>
12
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.js"></script>
13
+
14
+<script type="text/javascript" charset="utf-8">
15
+    $(document).ready(function() {
16
+        $('#assigned').dataTable();
17
+        $('#unassigned').dataTable();
18
+    } );
19
+</script>
20
+
21
+<div class="row">
22
+    <div class="col-lg-12">
23
+
24
+        <a href="{% url 'ticket_new' %}"><button type="button" class="btn btn-primary" style="float: right; margin-top: -50px; margin-right: 20px;">Create New Ticket</button></a>
25
+
26
+    <div class="page-header"><h1>All open Tickets</h1></div>
27
+
28
+    <table id="assigned" class="table table-striped table-bordered" cellspacing="0" width="100%">
29
+        <thead>
30
+        <tr>
31
+            <th>ID</th>
32
+            <th>Status</th>
33
+            <th>Owner</th>
34
+            <th>Assignee</th>
35
+            <th>Title</th>
36
+            <th>Description</th>
37
+        </tr>
38
+        </thead>
39
+
40
+        <tbody>
41
+    {% for ticket in tickets %}
42
+        <tr>
43
+            <td><a href="{% url 'ticket_detail' pk=ticket.id %}">{{ ticket.id }}</a></td>
44
+            <td>{% if ticket.status == "TODO" %}
45
+                   <span class="label label-danger">TODO</span>
46
+                {% elif ticket.status == "IN PROGRESS" %}
47
+                   <span class="label label-default">IN PROGRESS</span>
48
+                {% elif ticket.status == "WAITING" %}
49
+                   <span class="label label-warning">WAITING</span>
50
+                {% elif ticket.status == "DONE" %}
51
+                   <span class="label label-success">DONE</span>
52
+                {% else %}
53
+                {{ ticket.status }}
54
+                {% endif %}
55
+            </td>
56
+            <td>{{ ticket.owner }}</td>
57
+            <td>{% if ticket.assigned_to %}{{ ticket.assigned_to }}{% else %}---{% endif %}</td>
58
+            <td>{{ ticket.title }}</td>
59
+            <td>{{ ticket.description }}</td>
60
+        </tr>
61
+    {% endfor %}
62
+    </tbody></table>
63
+
64
+
65
+    </div>
66
+</div>
67
+
68
+{% endblock %}

+ 56
- 0
main/templates/main/archive.html View File

@@ -0,0 +1,56 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block title %}Tickets - Archive{% endblock %}
4
+
5
+{% block header_icon %}<i class="fa fa-file-text-o fa-5x"></i>{% endblock %}
6
+{% block headline %}Archive{% endblock %}
7
+{% block head-message %}Overview of all closed tickets in the system{% endblock %}
8
+
9
+{% block content %}
10
+<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.css">
11
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/1.10.4/js/jquery.dataTables.min.js"></script>
12
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.js"></script>
13
+
14
+<script type="text/javascript" charset="utf-8">
15
+    $(document).ready(function() {
16
+        $('#archived').dataTable();
17
+    } );
18
+</script>
19
+
20
+<div class="row">
21
+    <div class="col-lg-12">
22
+
23
+        <a href="{% url 'ticket_new' %}"><button type="button" class="btn btn-primary" style="float: right; margin-top: -50px; margin-right: 20px;">Create New Ticket</button></a>
24
+
25
+    <div class="page-header"><h1>Closed Tickets</h1></div>
26
+
27
+    <table id="archived" class="table table-striped table-bordered" cellspacing="0" width="100%">
28
+        <thead>
29
+        <tr>
30
+            <th>ID</th>
31
+            <th>Owner</th>
32
+            <th>Assignee</th>
33
+            <th>Title</th>
34
+            <th>Description</th>
35
+            <th>Closed</th>
36
+        </tr>
37
+        </thead>
38
+
39
+        <tbody>
40
+    {% for ticket in tickets %}
41
+        <tr>
42
+            <td><a href="{% url 'ticket_detail' pk=ticket.id %}">{{ ticket.id }}</a></td>
43
+            <td>{{ ticket.owner }}</td>
44
+            <td>{{ ticket.assigned_to }}</td>
45
+            <td>{{ ticket.title }}</td>
46
+            <td>{{ ticket.description }}</td>
47
+            <td>{{ ticket.closed_date|date:"d.m.Y, G:i" }}</td>
48
+        </tr>
49
+    {% endfor %}
50
+    </tbody></table>
51
+
52
+
53
+    </div>
54
+</div>
55
+
56
+{% endblock %}

+ 22
- 0
main/templates/main/attachment_add.html View File

@@ -0,0 +1,22 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block title %}Tickets - Add attachment{% endblock %}
4
+
5
+{% block header_icon %}<i class="fa fa-pencil-square-o fa-5x"></i>{% endblock %}
6
+{% block headline %}Add Attachment{% endblock %}
7
+{% block head-message %}Please fill out the form{% endblock %}
8
+
9
+{% block content %}
10
+
11
+    {% load crispy_forms_tags %}
12
+
13
+    <div class="page-header"><h1>Add Attachment</h1></div>
14
+
15
+    <form enctype="multipart/form-data" method="post" action="">
16
+        {% csrf_token %}
17
+        {{ form | crispy }}
18
+        <input class="btn btn-primary" type="submit" value="Save Attachment" />
19
+    </form>
20
+
21
+{% endblock %}
22
+

+ 112
- 0
main/templates/main/base.html View File

@@ -0,0 +1,112 @@
1
+{% load staticfiles %}
2
+
3
+<!DOCTYPE html>
4
+<html lang="en">
5
+<head>
6
+    <meta charset="utf-8">
7
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
+    <meta name="viewport" content="width=device-width, initial-scale=1">
9
+
10
+    <!-- Bootstrap -->
11
+    <link href="{% static "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet">
12
+
13
+    <!-- Font Awesome -->
14
+    <link href="{% static "font-awesome/css/font-awesome.min.css" %}" rel="stylesheet">
15
+
16
+    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
17
+    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
18
+    <!--[if lt IE 9]>
19
+      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
20
+      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
21
+    <![endif]-->
22
+
23
+    <title>{% block title %}{% endblock %}</title>
24
+    <link rel="stylesheet" type="text/css" href="{% static "css/style.css" %}" media="screen" />
25
+    <link rel="stylesheet" type="text/css" href="{% static "css/print.css" %}" media="print" />
26
+
27
+    <script src="{% static "jquery/jquery-1.11.1.min.js" %}"></script>
28
+</head>
29
+
30
+<body>
31
+
32
+<!-- Navigation -->
33
+<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
34
+    <div class="container">
35
+        <!-- Brand and toggle get grouped for better mobile display -->
36
+        <div class="navbar-header">
37
+            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
38
+                <span class="sr-only">Toggle navigation</span>
39
+                <span class="icon-bar"></span>
40
+                <span class="icon-bar"></span>
41
+                <span class="icon-bar"></span>
42
+            </button>
43
+            <!-- <a class="navbar-brand" href="/">Tickets</a> -->
44
+        </div>
45
+        <!-- Collect the nav links, forms, and other content for toggling -->
46
+        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
47
+            <ul class="nav navbar-nav">
48
+                <li><a href="/inbox/" title="Inbox"><i class="fa fa-home"></i></a></li>
49
+                <li><a href="/my-tickets/" title="My Tickets"><i class="fa fa-user"></i></a></li>
50
+                <li><a href="/all-tickets/" title="All Tickets"><i class="fa fa-list"></i></a></li>
51
+                <li><a href="/archive/" title="Archive"><i class="fa fa-archive"></i></a></li>
52
+                </ul>
53
+            <ul class="nav navbar-nav navbar-right" style="padding-right: 20px;">
54
+                <li><a href="/logout/" title="Logout"><i class="fa fa-sign-out"></i></a></li>
55
+            </ul>
56
+        </div>
57
+        <!-- /.navbar-collapse -->
58
+    </div>
59
+    <!-- /.container -->
60
+</nav>
61
+
62
+
63
+<!-- Page Content -->
64
+<div class="container">
65
+
66
+    <div class="row">
67
+        <div class="col-lg-12">
68
+
69
+            <div class="jumbotron" style="padding: 0px; margin-bottom: 0px;">
70
+                <div class="container">
71
+                    <div id="header_icon">{% block header_icon %}{% endblock %}</div>
72
+                    <h1>{% block headline %}{% endblock %}</h1>
73
+                    <p id="header_subtitle">{% block head-message %}{% endblock %}</p>
74
+                </div>
75
+            </div>
76
+
77
+        </div>
78
+    </div><!-- Ende row -->
79
+
80
+    {% block breadcrum %}
81
+    {% endblock %}
82
+
83
+    {% block content %}
84
+    {% endblock %}
85
+
86
+    <hr>
87
+
88
+    <!-- Footer -->
89
+    <footer>
90
+        <div class="row">
91
+            <div class="col-lg-6">
92
+                <p>Powered by <a href="https://www.python.org/" target=_new>Python</a>, <a href="https://www.djangoproject.com/" target=_new>Django</a> and <a href="http://getbootstrap.com/" target="_new">Bootstrap</a></p>
93
+            </div>
94
+            <div class="col-lg-6 text-right">
95
+                <p>License: <a href="https://github.com/suenkler/django-tickets">MIT</a></p>
96
+            </div>
97
+        </div>
98
+        <!-- /.row -->
99
+    </footer>
100
+
101
+</div>
102
+<!-- /.container -->
103
+
104
+<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
105
+<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> -->
106
+
107
+<!-- Include all compiled plugins (below), or include individual files as needed -->
108
+<script src="{% static "bootstrap/js/bootstrap.min.js" %}"></script>
109
+
110
+</body>
111
+
112
+</html>

+ 33
- 0
main/templates/main/followup_edit.html View File

@@ -0,0 +1,33 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block title %}Tickets - Edit followup{% endblock %}
4
+
5
+{% block header_icon %}<i class="fa fa-pencil-square-o fa-5x"></i>{% endblock %}
6
+{% block headline %}{% if 'edit' in request.path %}Edit Followup{% else %}Create new Followup{% endif %}{% endblock %}
7
+{% block head-message %}Please fill out the form{% endblock %}
8
+
9
+{% block content %}
10
+
11
+    {% load crispy_forms_tags %}
12
+
13
+{% if 'edit' in request.path %}
14
+
15
+    <div class="page-header"><h1>Edit Followup</h1></div>
16
+
17
+    <form action="#" method="post">
18
+        {% csrf_token %}
19
+        {{ form | crispy }}
20
+        <input class="btn btn-primary" type="submit" value="Save Followup" />
21
+    </form>
22
+{% else %}
23
+    <div class="page-header"><h1>Create a new Followup</h1></div>
24
+
25
+    <form action="/followup/new/" method="post">
26
+        {% csrf_token %}
27
+        {{ form | crispy }}
28
+        <input class="btn btn-primary" type="submit" value="Create new Followup" />
29
+    </form>
30
+{% endif %}
31
+
32
+{% endblock %}
33
+

+ 55
- 0
main/templates/main/inbox.html View File

@@ -0,0 +1,55 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block title %}Tickets - Inbox{% endblock %}
4
+
5
+{% block header_icon %}<i class="fa fa-file-text-o fa-5x"></i>{% endblock %}
6
+{% block headline %}Incoming Tickets{% endblock %}
7
+{% block head-message %}Feel free to pick up tickets on your own{% endblock %}
8
+
9
+
10
+{% block content %}
11
+<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.css">
12
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/1.10.4/js/jquery.dataTables.min.js"></script>
13
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.js"></script>
14
+
15
+<script type="text/javascript" charset="utf-8">
16
+    $(document).ready(function() {
17
+        $('#assigned').dataTable();
18
+        $('#unassigned').dataTable();
19
+    } );
20
+</script>
21
+
22
+<div class="row">
23
+    <div class="col-lg-12">
24
+
25
+        <a href="{% url 'ticket_new' %}"><button type="button" class="btn btn-primary" style="float: right; margin-top: -50px; margin-right: 20px;">Create New Ticket</button></a>
26
+
27
+    <div class="page-header"><h1>Tickets that haven't been yet assigned to anybody</h1></div>
28
+
29
+    <table id="unassigned" class="table table-striped table-bordered" cellspacing="0" width="100%">
30
+        <thead>
31
+        <tr>
32
+            <th>ID</th>
33
+            <th>Owner</th>
34
+            <th>Title</th>
35
+            <th>Description</th>
36
+        </tr>
37
+        </thead>
38
+
39
+        <tbody>
40
+    {% for ticket in tickets_unassigned %}
41
+        <tr>
42
+            <td><a href="{% url 'ticket_detail' pk=ticket.id %}">{{ ticket.id }}</a></td>
43
+            <td>{{ ticket.owner.first_name }} {{ ticket.owner.last_name }}</td>
44
+            <td>{{ ticket.title }}</td>
45
+            <td>{{ ticket.description }}</td>
46
+        </tr>
47
+    {% endfor %}
48
+
49
+    </tbody></table>
50
+
51
+
52
+    </div>
53
+</div>
54
+
55
+{% endblock %}

+ 117
- 0
main/templates/main/my-tickets.html View File

@@ -0,0 +1,117 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block title %}Tickets - My tickets{% endblock %}
4
+
5
+{% block header_icon %}<i class="fa fa-file-text-o fa-5x"></i>{% endblock %}
6
+{% block headline %}My Tickets{% endblock %}
7
+{% block head-message %}Tickets assigned to you personally{% endblock %}
8
+
9
+{% block content %}
10
+<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.css">
11
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/1.10.4/js/jquery.dataTables.min.js"></script>
12
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.js"></script>
13
+
14
+<script type="text/javascript" charset="utf-8">
15
+    $(document).ready(function() {
16
+        $('#assigned').dataTable();
17
+        $('#unassigned').dataTable();
18
+    } );
19
+</script>
20
+
21
+<div class="row">
22
+    <div class="col-lg-12">
23
+
24
+        <a href="{% url 'ticket_new' %}?next={{ request.path }}"><button type="button" class="btn btn-primary" style="float: right; margin-top: -50px; margin-right: 20px;">Create New Ticket</button></a>
25
+
26
+        {% if tickets_waiting %}
27
+
28
+        <div class="alert alert-danger" role="alert" style="margin-top: 20px;"><b>Collegues are waiting for your input!</b><br/><br/>
29
+
30
+          <table id="waiting" class="table table-bordered" cellspacing="0" width="100%">
31
+            <thead>
32
+              <tr>
33
+                <th>ID</th>
34
+                <th>Status</th>
35
+                <th>Owner</th>
36
+                <th>Assignee</th>
37
+                <th>Title</th>
38
+                <th>Description</th>
39
+              </tr>
40
+            </thead>
41
+
42
+            <tbody>
43
+              {% for ticket in tickets_waiting %}
44
+              <tr>
45
+                <td><a href="{% url 'ticket_detail' pk=ticket.id %}">{{ ticket.id }}</a></td>
46
+                <td>{% if ticket.status == "TODO" %}
47
+                  <span class="label label-danger">TODO</span>
48
+                  {% elif ticket.status == "IN PROGRESS" %}
49
+                  <span class="label label-default">IN PROGRESS</span>
50
+                  {% elif ticket.status == "WAITING" %}
51
+                  <span class="label label-warning">WAITING</span>
52
+                  {% elif ticket.status == "DONE" %}
53
+                  <span class="label label-success">DONE</span>
54
+                  {% else %}
55
+                  {{ ticket.status }}
56
+                  {% endif %}
57
+                </td>
58
+                <td>{{ ticket.owner }}</td>
59
+                <td>{{ ticket.assigned_to }}</td>
60
+                <td>{{ ticket.title }}</td>
61
+                <td>{{ ticket.description }}</td>
62
+              </tr>
63
+              {% endfor %}
64
+
65
+            </tbody>
66
+          </table>
67
+
68
+        </div>
69
+        
70
+        {% endif %}
71
+
72
+        
73
+
74
+    <div class="page-header"><h1>Tickets assigned to myself</h1></div>
75
+
76
+    <table id="assigned" class="table table-striped table-bordered" cellspacing="0" width="100%">
77
+        <thead>
78
+        <tr>
79
+            <th>ID</th>
80
+            <th>Status</th>
81
+            <th>Owner</th>
82
+            <th>Title</th>
83
+            <th>Description</th>
84
+        </tr>
85
+        </thead>
86
+
87
+        <tbody>
88
+    {% for ticket in tickets %}
89
+        <tr>
90
+            <td><a href="{% url 'ticket_detail' pk=ticket.id %}">{{ ticket.id }}</a></td>
91
+            <td>{% if ticket.status == "TODO" %}
92
+                   <span class="label label-danger">TODO</span>
93
+                {% elif ticket.status == "IN PROGRESS" %}
94
+                   <span class="label label-default">IN PROGRESS</span>
95
+                {% elif ticket.status == "WAITING" %}
96
+                   <span class="label label-warning">WAITING</span>
97
+                {% elif ticket.status == "DONE" %}
98
+                   <span class="label label-success">DONE</span>
99
+                {% else %}
100
+                {{ ticket.status }}
101
+                {% endif %}
102
+            </td>
103
+            <td>{{ ticket.owner }}</td>
104
+            <td>{{ ticket.title }}</td>
105
+            <td>{{ ticket.description }}</td>
106
+        </tr>
107
+    {% endfor %}
108
+
109
+
110
+        </tbody>
111
+    </table>
112
+
113
+
114
+    </div>
115
+</div>
116
+
117
+{% endblock %}

+ 32
- 0
main/templates/main/settings.html View File

@@ -0,0 +1,32 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block title %}Tickets - Settings{% endblock %}
4
+
5
+{% block header_icon %}<i class="fa fa-cogs fa-5x"></i>{% endblock %}
6
+{% block headline %}Settings{% endblock %}
7
+{% block head-message %}Please adjust your settings{% endblock %}
8
+
9
+{% block content %}
10
+{% load staticfiles %}
11
+
12
+<div class="row" style="margin-top: 30px;">
13
+    <div class="col-md-6">
14
+
15
+        {% load crispy_forms_tags %}
16
+
17
+        <form action="" method="post">
18
+            {% csrf_token %}
19
+
20
+            {{ form_user|crispy }}
21
+      <br/>
22
+    </div>
23
+</div>
24
+
25
+<div class="row">
26
+    <div class="col-md-12">
27
+        <input type="submit" value="Save Settings" class="btn btn-primary" />
28
+        </form>
29
+    </div>
30
+</div>
31
+
32
+{% endblock %}

+ 113
- 0
main/templates/main/ticket_detail.html View File

@@ -0,0 +1,113 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block header_icon %}<i class="fa fa-pencil-square-o fa-5x"></i>{% endblock %}
4
+{% block headline %}Ticket #{{ticket.id}}{% endblock %}
5
+{% block head-message %}Everything you need to know about this ticket{% endblock %}
6
+
7
+{% block title %}Tickets - Details of ticket #{{ ticket.id }}{% endblock %}
8
+
9
+{% block content %}
10
+
11
+<div class="dropdown" style="float: right; margin-top: -50px; margin-right: 20px;">
12
+  <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-expanded="true">
13
+    Actions
14
+    <span class="caret"></span>
15
+  </button>
16
+  <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
17
+      <li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'ticket_edit' pk=ticket.id %}" id="edit_ticket">Edit ticket</a></li>
18
+      <li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'attachment_new' %}?ticket={{ ticket.id }}" id="add_attachment">Add attachment</a></li>
19
+      <li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'followup_new' %}?ticket={{ ticket.id }}" id="add-followup">Add followup</a></li>
20
+  </ul>
21
+</div>
22
+
23
+      <div class="page-header"><h1>Ticket #{{ ticket.id }}</h1></div>
24
+
25
+<div class="row">
26
+    <div class="col-lg-8">
27
+
28
+      <style>
29
+       .description { background: #EEE; }
30
+      </style>
31
+
32
+<table class="table table-bordered">
33
+  <tr>
34
+    <td class="description"><b>Status</b></td>
35
+    <td>{% if ticket.status == "TODO" %}<span class="label label-danger">TODO</span>{% elif ticket.status == "DONE" %}<span class="label label-success">DONE</span>{% elif ticket.status == "WAITING" %}<span class="label label-warning">WAITING</span>{% elif ticket.status == "IN PROGRESS" %}<span class="label label-default">IN PROGRESS</span>{% endif %}</td>
36
+  </tr>
37
+  <tr>
38
+    <td class="description"><b>Assigned to</b></td>
39
+    <td>{% if ticket.assigned_to %}{{ ticket.assigned_to.first_name }} {{ ticket.assigned_to.last_name }}{% else %}---{% endif %}</td>
40
+  </tr>
41
+  <tr>
42
+    <td class="description"><b>Owner</b></td>
43
+    <td>{{ ticket.owner.first_name }} {{ ticket.owner.last_name }}</td>
44
+  </tr>
45
+  <tr>
46
+    <td width="180px" class="description"><b>Title</b></td>
47
+    <td>{{ ticket.title }}</td>
48
+  </tr>
49
+  <tr>
50
+    <td class="description"><b>Description</b></td>
51
+    <td>{{ ticket.description}}</td>
52
+  </tr>
53
+</table>
54
+
55
+    </div>
56
+    <div class="col-lg-4">
57
+
58
+{% if attachments %}
59
+<h2>Attachments</h2>
60
+<ul>
61
+    {% for attachment in attachments %}
62
+    <li><a href="/media/tickets/{{ ticket.id }}/{{ attachment.filename }}">{{ attachment.filename }}</a></li>
63
+    {% endfor %}
64
+</ul>
65
+{% endif %}
66
+
67
+    </div><!-- Ende column -->
68
+</div><!-- Ende row -->
69
+
70
+<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.css">
71
+
72
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/1.10.4/js/jquery.dataTables.min.js"></script>
73
+
74
+<script type="text/javascript" language="javascript" src="//cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.js"></script>
75
+
76
+<script type="text/javascript" charset="utf-8">
77
+    $(document).ready(function() {
78
+        $('#followups').dataTable();
79
+    } );
80
+</script>
81
+
82
+<h2>Followups</h2>
83
+{% if followups %}
84
+<table id="followups" class="table table-striped table-bordered" cellspacing="0" width="100%">
85
+    <thead>
86
+    <tr>
87
+        <th width="10px"></th>
88
+        <th width="150px">User</th>
89
+        <th>Text</th>
90
+        <th width="100px">Modified</th>
91
+    </tr>
92
+    </thead>
93
+    <tbody>
94
+    {% for followup in followups %}
95
+    <tr>
96
+      <td>
97
+          <a href="{% url 'followup_edit' pk=followup.id %}"><i class="fa fa-pencil-square-o"></i></a>
98
+      </td>
99
+      <td>{{ followup.user.first_name }} {{ followup.user.last_name }}</td>
100
+      <td>{{ followup.text }}</td>
101
+      <td>{{ followup.modified|date:"d.m.Y, G:i" }}</td>
102
+    </tr>
103
+    {% endfor %}
104
+    </tbody>
105
+</table>
106
+
107
+{% else %}
108
+    <p>no followup so far...</p>
109
+{% endif %}
110
+
111
+
112
+{% endblock %}
113
+

+ 33
- 0
main/templates/main/ticket_edit.html View File

@@ -0,0 +1,33 @@
1
+{% extends "main/base.html" %}
2
+
3
+{% block header_icon %}<i class="fa fa-pencil-square-o fa-5x"></i>{% endblock %}
4
+{% block headline %}{% if 'edit' in request.path %}Edit ticket{% else %}Create new ticket{% endif %}{% endblock %}
5
+{% block head-message %}Please fill out the form below{% endblock %}
6
+
7
+{% block title %}{% if 'edit' in request.path %}Tickets - Edit ticket{% else %}Tickets - Create new ticket{% endif %}{% endblock %}
8
+
9
+{% block content %}
10
+
11
+    {% load crispy_forms_tags %}
12
+
13
+{% if 'edit' in request.path %}
14
+
15
+    <div class="page-header"><h1>Edit Ticket</h1></div>
16
+
17
+    <form action="#" method="post">
18
+        {% csrf_token %}
19
+        {{ form | crispy }}
20
+        <input class="btn btn-primary" type="submit" value="Save Ticket" />
21
+    </form>
22
+{% else %}
23
+    <div class="page-header"><h1>Create a new Ticket</h1></div>
24
+
25
+    <form action="/ticket/new/" method="post">
26
+        {% csrf_token %}
27
+        {{ form | crispy }}
28
+        <input class="btn btn-primary" type="submit" value="Create new Ticket" />
29
+    </form>
30
+{% endif %}
31
+
32
+{% endblock %}
33
+

+ 190
- 0
main/templates/registration/login.html View File

@@ -0,0 +1,190 @@
1
+{% load staticfiles %}
2
+
3
+<!DOCTYPE html>
4
+<html lang="en">
5
+  <head>
6
+    <meta charset="utf-8">
7
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
+    <meta name="description" content="">
10
+    <meta name="author" content="">
11
+    <link rel="shortcut icon" href="{% static 'img/favicon.ico' %}">
12
+    <title>Django tickets</title>
13
+    <!-- Bootstrap core CSS -->
14
+    <link href="{% static 'bootstrap/css/bootstrap.css' %}" rel="stylesheet">
15
+    <!-- Custom styles for this site -->
16
+
17
+    <link href="{% static 'css/frontpage.css' %}" rel="stylesheet">
18
+    <!-- Custom tags for the head tag -->
19
+    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
20
+    <!--[if lt IE 9]>
21
+    <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
22
+    <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
23
+    <![endif]-->
24
+
25
+  </head>
26
+  <body>
27
+    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
28
+      <div class="container">
29
+        <div class="navbar-header">
30
+        </div>
31
+        <div class="collapse navbar-collapse">
32
+          <ul class="nav navbar-nav">
33
+          </ul>
34
+            <ul class="nav navbar-nav navbar-right">
35
+                <!-- <li><a href="{% url 'inbox' %}">Login</a></li> -->
36
+                <li><a href="#" title="Login" data-toggle="modal" data-target="#LoginModal">Login</a></li>
37
+          </ul>
38
+        </div><!--/.nav-collapse -->
39
+      </div>
40
+    </div>
41
+
42
+
43
+    <div class="jumbotron jumbotron-carousel corporate-jumbo">
44
+      <div class="container">
45
+        <div class="row">
46
+          <div class="col-md-8 col-sm-12">
47
+              <h1 style="color: white;">Tickets</h1>
48
+              <p>A simple ticketing application</p>
49
+          </div>
50
+        </div>
51
+      </div>
52
+    </div>
53
+
54
+    <div class="container">
55
+
56
+        <div class="row">
57
+        <div class="col-lg-4">
58
+          <h2><i class="fa fa-cloud"></i> Web based</h2>
59
+          <p>This is a web based ticketing application. So there is no need for local installations on the working stations of the users (besides a web browser).</p>
60
+        </div>
61
+        <div class="col-lg-4">
62
+          <h2><i class="fa fa-users"></i> Multi User</h2>
63
+          <p>It is a multi user application. Every user can create tickets, assign tickets to other users, comment tickets, change the ticket status and so on.</p>
64
+        </div>
65
+        <div class="col-lg-4">
66
+          <h2><i class="fa fa-envelope"></i> Email integration</h2>
67
+          <p>One of the main features is the ability to create new tickets via email. The app checks an IMAP account for new email regularly and creates a ticket for every email found in the inbox.</p>
68
+        </div>
69
+      </div>
70
+
71
+        <div class="row">
72
+        <div class="col-lg-4">
73
+          <h2><i class="fa fa-lock"></i> Security</h2>
74
+          <p>Django is innately a very secure web framework. But there are some best practices of the django community that enhance security. In this application I tried to implement these good security best practices.</p>
75
+        </div>
76
+        <div class="col-lg-4">
77
+          <h2><i class="fa fa-code"></i> Python/Django</h2>
78
+          <p>The application is based on the programming language <a href="https://www.python.org/" target="new">Python</a> and the web framework <a href="https://www.djangoproject.com/" target="new">Django</a>.</p>
79
+        </div>
80
+        <div class="col-lg-4">
81
+          <h2><i class="fa fa-dot-circle-o"></i> Minimal Feature Set</h2>
82
+          <p>By now, this is just a very simple app with a minimal features set. It is not intended to grow up to a kitchen sink. But I will add some features in the future.</p>
83
+        </div>
84
+      </div>
85
+
86
+    </div>
87
+
88
+
89
+    <!-- Site footer -->
90
+    <!-- Some social button for contact will do -->
91
+    <a name="contact"></a>
92
+    <div class="container">
93
+      <div class="footer">
94
+        <div class="row">
95
+          <div class="col-lg-6">
96
+            <p>Powered by <a href="https://www.python.org/" target="_new">Python</a>, <a href="https://www.djangoproject.com/" target="_new">Django</a>, and <a href="http://getbootstrap.com/" target="_new">Bootstrap</a></p>
97
+          </div>
98
+          <div class="col-lg-6 text-right">
99
+            <p>License: <a href="https://github.com/suenkler/django-tickets">MIT</a></p>
100
+          </div>
101
+        </div>
102
+      </div>
103
+    </div>
104
+
105
+
106
+        <!-- Bootstrap Modal -->
107
+        <div class="modal fade" id="LoginModal" tabindex="-1" role="dialog" aria-labelledby="LoginModalLabel" aria-hidden="true">
108
+            <div class="modal-dialog" style="width: 450px;">
109
+                <div class="modal-content">
110
+                    <div class="modal-header">
111
+                        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
112
+                        <h4 class="modal-title" id="myModalLabel">Please sign in</h4>
113
+                    </div>
114
+                    <div class="modal-body">
115
+                        <form class="form-signin" role="form" method="post" action="{% url 'django.contrib.auth.views.login' %}">
116
+                            {% csrf_token %}
117
+                            <!-- <h2 class="form-signin-heading">Please sign in</h2> -->
118
+                            <label for="login-username" class="sr-only">User name</label>
119
+                            <input type="text" id="username" name="username" class="form-control" placeholder="User name" required autofocus>
120
+                            <label for="login-password" class="sr-only">Password</label>
121
+                            <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
122
+
123
+                            <button class="btn btn-lg btn-primary btn-block" value="login" type="submit">Sign in</button>
124
+                            <input type="hidden" name="next" value="{{ next }}" />
125
+                        </form>
126
+
127
+                    </div>
128
+                    <div class="modal-footer" style="text-align: left;">
129
+                        Forgot your password? Shit happens.
130
+                    </div>
131
+                </div>
132
+            </div>
133
+        </div>
134
+
135
+
136
+
137
+
138
+
139
+        <!-- Bootstrap Modal -->
140
+        <div class="modal fade" id="ErrorModal" tabindex="-1" role="dialog" aria-labelledby="ErrorModalLabel" aria-hidden="true">
141
+            <div class="modal-dialog" style="width: 450px;">
142
+                <div class="modal-content">
143
+
144
+                    <div class="modal-body">
145
+
146
+                        <div class="alert alert-danger" role="alert" style="background: #f1f1f1;"><i class="fa fa-frown-o" style="float: right; color: red; padding: 10px; margin-left: 10px; font-size: 80px;"></i><b>Ooooops!</b><br/>Your credentials are invalid! Please <a href="#" title="Login" data-toggle="modal" data-target="#LoginModal" id="LinkLogin">try again</a>. If you have any questions, please do not contact me.</div>
147
+
148
+
149
+                    </div>
150
+
151
+                </div>
152
+            </div>
153
+        </div>
154
+
155
+
156
+    <!-- Bootstrap core JavaScript
157
+    ================================================== -->
158
+    <!-- Placed at the end of the document so the pages load faster -->
159
+    <script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
160
+    <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
161
+
162
+
163
+{% if form.errors %}
164
+<script>
165
+
166
+/* Modal mit Error-Message beim Laden der Seite öffnen, wenn es Form Errors gibt. */
167
+$('#ErrorModal').modal()
168
+
169
+/* Wenn dann im Error-Modal auf "try again" geklickt wird, soll das Error-Modal
170
+   geschlossen und das Login-Modal wieder geöffnet werden. */
171
+$('#LinkLogin').click( function() {
172
+    $('#ErrorModal').modal('hide');
173
+    $('#LoginModal').modal('show');
174
+    /* $('#username').focus(); */
175
+    return false; } );
176
+
177
+</script>
178
+{% endif %}
179
+
180
+<script>
181
+    /* Autofocus field with 'autofocus' attribute in Bootstrap Modals */
182
+    $('.modal').on('shown.bs.modal', function() {
183
+        $(this).find('[autofocus]').focus();
184
+    });
185
+</script>
186
+
187
+  </body>
188
+</html>
189
+
190
+

+ 3
- 0
main/tests.py View File

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

+ 222
- 0
main/views.py View File

@@ -0,0 +1,222 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.shortcuts import render, render_to_response, redirect
4
+from django.template import RequestContext
5
+from django.contrib.auth.models import User
6
+from django.http import HttpResponseRedirect
7
+from django.utils import timezone
8
+from django.core.mail import send_mail
9
+
10
+from .models import Ticket, Attachment, FollowUp
11
+from .forms import UserSettingsForm
12
+from .forms import TicketCreateForm, TicketEditForm, FollowupForm, AttachmentForm
13
+
14
+# Logging
15
+import logging
16
+logger = logging.getLogger(__name__)
17
+
18
+
19
+def inbox_view(request):
20
+
21
+    users = User.objects.all()
22
+    tickets_unassigned = Ticket.objects.all().exclude(assigned_to__in=users)
23
+    tickets_assigned = Ticket.objects.filter(assigned_to__in=users)
24
+
25
+    return render_to_response('main/inbox.html',
26
+                              {"tickets_assigned": tickets_assigned,
27
+                               "tickets_unassigned": tickets_unassigned, },
28
+                              context_instance=RequestContext(request))
29
+
30
+
31
+def my_tickets_view(request):
32
+
33
+    tickets = Ticket.objects.filter(assigned_to=request.user).exclude(status__exact="DONE")
34
+    tickets_waiting = Ticket.objects.filter(waiting_for=request.user).filter(status__exact="WAITING")
35
+
36
+    
37
+    return render_to_response('main/my-tickets.html',
38
+                              {"tickets": tickets,
39
+                               "tickets_waiting": tickets_waiting },
40
+                              context_instance=RequestContext(request))
41
+
42
+
43
+def all_tickets_view(request):
44
+
45
+    tickets_open = Ticket.objects.all().exclude(status__exact="DONE")
46
+
47
+    return render_to_response('main/all-tickets.html',
48
+                              {"tickets": tickets_open, },
49
+                              context_instance=RequestContext(request))
50
+
51
+
52
+def archive_view(request):
53
+
54
+    tickets_closed = Ticket.objects.filter(status__exact="DONE")
55
+
56
+    return render_to_response('main/archive.html',
57
+                              {"tickets": tickets_closed, },
58
+                              context_instance=RequestContext(request))
59
+
60
+
61
+def usersettings_update_view(request):
62
+
63
+    user = request.user
64
+
65
+    if request.method == 'POST':
66
+        # create a form instance and populate it with data from the request:
67
+        form_user = UserSettingsForm(request.POST)
68
+
69
+        # check whether it's valid:
70
+        if form_user.is_valid():
71
+
72
+            # Save User model fields
73
+            user.first_name = request.POST['first_name']
74
+            user.last_name = request.POST['last_name']
75
+            user.save()
76
+
77
+            # redirect to the index page
78
+            return HttpResponseRedirect(request.GET.get('next', '/inbox/'))
79
+
80
+    # if a GET (or any other method) we'll create a blank form
81
+    else:
82
+        form_user = UserSettingsForm(instance=user)
83
+
84
+    return render(request, 'main/settings.html', {'form_user': form_user,})
85
+
86
+
87
+def ticket_create_view(request):
88
+
89
+    if request.POST:
90
+        form = TicketCreateForm(request.POST)
91
+
92
+        if form.is_valid():
93
+
94
+            obj = form.save()
95
+            # set owner
96
+            obj.owner = request.user
97
+            obj.status = "TODO"
98
+            obj.save()
99
+
100
+            return redirect('inbox')
101
+
102
+    else:
103
+        form = TicketCreateForm()
104
+
105
+    return render(request,
106
+                  'main/ticket_edit.html',
107
+                  {'form': form, })
108
+
109
+
110
+def ticket_edit_view(request, pk):
111
+
112
+    data = Ticket.objects.get(id=pk)
113
+
114
+    if request.POST:
115
+        form = TicketEditForm(request.POST, instance=data)
116
+        if form.is_valid():
117
+
118
+            # set field closed_date to now() if status changed to "DONE"
119
+            if form.cleaned_data['status'] == "DONE":
120
+                data.closed_date = timezone.now()
121
+
122
+            form.save()
123
+
124
+            return redirect('inbox')
125
+
126
+    else:
127
+        form = TicketEditForm(instance=data)
128
+
129
+    return render(request,
130
+                  'main/ticket_edit.html',
131
+                  {'form': form, })
132
+
133
+
134
+def ticket_detail_view(request, pk):
135
+
136
+    ticket = Ticket.objects.get(id=pk)
137
+    attachments = Attachment.objects.filter(ticket=ticket)
138
+    followups = FollowUp.objects.filter(ticket=ticket)
139
+
140
+    return render(request,
141
+                  'main/ticket_detail.html',
142
+                  {'ticket': ticket,
143
+                   'attachments': attachments,
144
+                   'followups': followups, })
145
+
146
+
147
+def followup_create_view(request):
148
+
149
+    if request.POST:
150
+
151
+        form = FollowupForm(request.POST)
152
+
153
+        if form.is_valid():
154
+            form.save()
155
+
156
+            ticket = Ticket.objects.get(id=request.POST['ticket'])
157
+            # mail notification to owner of ticket
158
+            notification_subject = "[#" + str(ticket.id) + "] New followup"
159
+            notification_body = "Hi,\n\na new followup was created for ticket #" \
160
+                                + str(ticket.id) \
161
+                                + " (http://localhost:8000/ticket/" \
162
+                                + str(ticket.id) \
163
+                                + "/)\n\nTitle: " + form.data['title'] \
164
+                                + "\n\n" + form.data['text']
165
+
166
+            send_mail(notification_subject, notification_body, 'test@suenkler.info',
167
+                            [ticket.owner.email], fail_silently=False)
168
+
169
+            return redirect('inbox')
170
+
171
+    else:
172
+        form = FollowupForm(initial={'ticket': request.GET.get('ticket'),
173
+                                     'user': request.user})
174
+
175
+    return render(request,
176
+                  'main/followup_edit.html',
177
+                  {'form': form, })
178
+
179
+
180
+def followup_edit_view(request, pk):
181
+
182
+    data = FollowUp.objects.get(id=pk)
183
+
184
+    if request.POST:
185
+        form = FollowupForm(request.POST, instance=data)
186
+        if form.is_valid():
187
+            form.save()
188
+
189
+            return redirect('inbox')
190
+
191
+    else:
192
+        form = FollowupForm(instance=data)
193
+
194
+    return render(request,
195
+                  'main/followup_edit.html',
196
+                  {'form': form, })
197
+
198
+
199
+def attachment_create_view(request):
200
+
201
+    if request.POST:
202
+        form = AttachmentForm(request.POST, request.FILES)
203
+
204
+        if form.is_valid():
205
+            attachment = Attachment(
206
+                ticket=Ticket.objects.get(id=request.GET['ticket']),
207
+                file=request.FILES['file'],
208
+                filename=request.FILES['file'].name,
209
+                user=request.user
210
+                #mime_type=form.file.get_content_type(),
211
+                #size=len(form.file),
212
+            )
213
+            attachment.save()
214
+
215
+            return redirect('inbox')
216
+
217
+    else:
218
+        form = AttachmentForm()
219
+
220
+    return render(request,
221
+                  'main/attachment_add.html',
222
+                  {'form': form, })

+ 10
- 0
manage.py View File

@@ -0,0 +1,10 @@
1
+#!/usr/bin/env python
2
+import os
3
+import sys
4
+
5
+if __name__ == "__main__":
6
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tickets.settings")
7
+
8
+    from django.core.management import execute_from_command_line
9
+
10
+    execute_from_command_line(sys.argv)

+ 5
- 0
requirements.txt View File

@@ -0,0 +1,5 @@
1
+Django==1.8.4
2
+argparse==1.2.1
3
+django-crispy-forms==1.5.1
4
+email-reply-parser==0.3.0
5
+wsgiref==0.1.2

BIN
screenshots/screenshot_all_open_tickets.png View File


BIN
screenshots/screenshot_archive.png View File


BIN
screenshots/screenshot_detail_view.png View File


BIN
screenshots/screenshot_inbox.png View File


BIN
screenshots/screenshot_landing_page.png View File


BIN
screenshots/screenshot_login.png View File


BIN
screenshots/screenshot_my_tickets.png View File


+ 61
- 0
static/500.html View File

@@ -0,0 +1,61 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="utf-8">
5
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+    <meta name="description" content="">
8
+    <meta name="author" content="">
9
+    <link rel="shortcut icon" href="/static/img/favicon.ico">
10
+    <title>bepartners</title>
11
+    <!-- Bootstrap core CSS -->
12
+    <link href="/static/bootstrap/css/bootstrap.css" rel="stylesheet">
13
+    <!-- Custom styles for this site -->
14
+
15
+    <link href="/static/css/frontpage.css" rel="stylesheet">
16
+    <!-- Custom tags for the head tag -->
17
+    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
18
+    <!--[if lt IE 9]>
19
+    <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
20
+    <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
21
+    <![endif]-->
22
+
23
+  </head>
24
+  <body>
25
+
26
+    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
27
+      <div class="container">
28
+        <div class="navbar-header">
29
+        </div>
30
+        <div class="collapse navbar-collapse">
31
+          <ul class="nav navbar-nav">
32
+          </ul>
33
+            <ul class="nav navbar-nav navbar-right">
34
+          </ul>
35
+        </div><!--/.nav-collapse -->
36
+      </div>
37
+    </div>
38
+
39
+    <div class="container">
40
+
41
+        <div class="row">
42
+            <div class="col-lg-1">
43
+            </div>
44
+            <div class="col-lg-10">
45
+                <h1 style="font-size: 50px; margin-top: 150px;">Entschuldigung!</h1>
46
+                <div style="color: grey; font-size: 20px; margin-top: 30px;">Es ist ein Fehler aufgetreten. Unsere Entwickler wurden per E-Mail über diesen Vorfall informiert und werden das Problem schnellstmöglich beheben.<br/><br/>Bei Fragen kontaktieren Sie uns gerne per <a href="mailto:mailbox@suenkler.info">E-Mail</a>.</div>
47
+            </div>
48
+            <div class="col-lg-1">
49
+            </div>
50
+        </div>
51
+
52
+    </div>
53
+
54
+    <!-- Bootstrap core JavaScript
55
+    ================================================== -->
56
+    <!-- Placed at the end of the document so the pages load faster -->
57
+    <script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
58
+    <script src="/static/bootstrap/js/bootstrap.min.js"></script>
59
+
60
+  </body>
61
+</html>

+ 457
- 0
static/bootstrap/css/bootstrap-theme.css View File

@@ -0,0 +1,457 @@
1
+/*!
2
+ * Bootstrap v3.3.0 (http://getbootstrap.com)
3
+ * Copyright 2011-2014 Twitter, Inc.
4
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5
+ */
6
+
7
+.btn-default,
8
+.btn-primary,
9
+.btn-success,
10
+.btn-info,
11
+.btn-warning,
12
+.btn-danger {
13
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
14
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
15
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
16
+}
17
+.btn-default:active,
18
+.btn-primary:active,
19
+.btn-success:active,
20
+.btn-info:active,
21
+.btn-warning:active,
22
+.btn-danger:active,
23
+.btn-default.active,
24
+.btn-primary.active,
25
+.btn-success.active,
26
+.btn-info.active,
27
+.btn-warning.active,
28
+.btn-danger.active {
29
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
30
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
31
+}
32
+.btn-default .badge,
33
+.btn-primary .badge,
34
+.btn-success .badge,
35
+.btn-info .badge,
36
+.btn-warning .badge,
37
+.btn-danger .badge {
38
+  text-shadow: none;
39
+}
40
+.btn:active,
41
+.btn.active {
42
+  background-image: none;
43
+}
44
+.btn-default {
45
+  text-shadow: 0 1px 0 #fff;
46
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
47
+  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
48
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
49
+  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
50
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
51
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
52
+  background-repeat: repeat-x;
53
+  border-color: #dbdbdb;
54
+  border-color: #ccc;
55
+}
56
+.btn-default:hover,
57
+.btn-default:focus {
58
+  background-color: #e0e0e0;
59
+  background-position: 0 -15px;
60
+}
61
+.btn-default:active,
62
+.btn-default.active {
63
+  background-color: #e0e0e0;
64
+  border-color: #dbdbdb;
65
+}
66
+.btn-default:disabled,
67
+.btn-default[disabled] {
68
+  background-color: #e0e0e0;
69
+  background-image: none;
70
+}
71
+.btn-primary {
72
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
73
+  background-image:      -o-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
74
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#2d6ca2));
75
+  background-image:         linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
76
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
77
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
78
+  background-repeat: repeat-x;
79
+  border-color: #2b669a;
80
+}
81
+.btn-primary:hover,
82
+.btn-primary:focus {
83
+  background-color: #2d6ca2;
84
+  background-position: 0 -15px;
85
+}
86
+.btn-primary:active,
87
+.btn-primary.active {
88
+  background-color: #2d6ca2;
89
+  border-color: #2b669a;
90
+}
91
+.btn-primary:disabled,
92
+.btn-primary[disabled] {
93
+  background-color: #2d6ca2;
94
+  background-image: none;
95
+}
96
+.btn-success {
97
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
98
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
99
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
100
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
101
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
102
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
103
+  background-repeat: repeat-x;
104
+  border-color: #3e8f3e;
105
+}
106
+.btn-success:hover,
107
+.btn-success:focus {
108
+  background-color: #419641;
109
+  background-position: 0 -15px;
110
+}
111
+.btn-success:active,
112
+.btn-success.active {
113
+  background-color: #419641;
114
+  border-color: #3e8f3e;
115
+}
116
+.btn-success:disabled,
117
+.btn-success[disabled] {
118
+  background-color: #419641;
119
+  background-image: none;
120
+}
121
+.btn-info {
122
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
123
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
124
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
125
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
126
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
127
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
128
+  background-repeat: repeat-x;
129
+  border-color: #28a4c9;
130
+}
131
+.btn-info:hover,
132
+.btn-info:focus {
133
+  background-color: #2aabd2;
134
+  background-position: 0 -15px;
135
+}
136
+.btn-info:active,
137
+.btn-info.active {
138
+  background-color: #2aabd2;
139
+  border-color: #28a4c9;
140
+}
141
+.btn-info:disabled,
142
+.btn-info[disabled] {
143
+  background-color: #2aabd2;
144
+  background-image: none;
145
+}
146
+.btn-warning {
147
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
148
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
149
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
150
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
151
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
152
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
153
+  background-repeat: repeat-x;
154
+  border-color: #e38d13;
155
+}
156
+.btn-warning:hover,
157
+.btn-warning:focus {
158
+  background-color: #eb9316;
159
+  background-position: 0 -15px;
160
+}
161
+.btn-warning:active,
162
+.btn-warning.active {
163
+  background-color: #eb9316;
164
+  border-color: #e38d13;
165
+}
166
+.btn-warning:disabled,
167
+.btn-warning[disabled] {
168
+  background-color: #eb9316;
169
+  background-image: none;
170
+}
171
+.btn-danger {
172
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
173
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
174
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
175
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
176
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
177
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
178
+  background-repeat: repeat-x;
179
+  border-color: #b92c28;
180
+}
181
+.btn-danger:hover,
182
+.btn-danger:focus {
183
+  background-color: #c12e2a;
184
+  background-position: 0 -15px;
185
+}
186
+.btn-danger:active,
187
+.btn-danger.active {
188
+  background-color: #c12e2a;
189
+  border-color: #b92c28;
190
+}
191
+.btn-danger:disabled,
192
+.btn-danger[disabled] {
193
+  background-color: #c12e2a;
194
+  background-image: none;
195
+}
196
+.thumbnail,
197
+.img-thumbnail {
198
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
199
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
200
+}
201
+.dropdown-menu > li > a:hover,
202
+.dropdown-menu > li > a:focus {
203
+  background-color: #e8e8e8;
204
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
205
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
206
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
207
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
208
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
209
+  background-repeat: repeat-x;
210
+}
211
+.dropdown-menu > .active > a,
212
+.dropdown-menu > .active > a:hover,
213
+.dropdown-menu > .active > a:focus {
214
+  background-color: #357ebd;
215
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
216
+  background-image:      -o-linear-gradient(top, #428bca 0%, #357ebd 100%);
217
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd));
218
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
219
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
220
+  background-repeat: repeat-x;
221
+}
222
+.navbar-default {
223
+  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
224
+  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
225
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
226
+  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
227
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
228
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
229
+  background-repeat: repeat-x;
230
+  border-radius: 4px;
231
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
232
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
233
+}
234
+.navbar-default .navbar-nav > .open > a,
235
+.navbar-default .navbar-nav > .active > a {
236
+  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
237
+  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
238
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
239
+  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
240
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
241
+  background-repeat: repeat-x;
242
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
243
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
244
+}
245
+.navbar-brand,
246
+.navbar-nav > li > a {
247
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
248
+}
249
+.navbar-inverse {
250
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
251
+  background-image:      -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
252
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
253
+  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
254
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
255
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
256
+  background-repeat: repeat-x;
257
+}
258
+.navbar-inverse .navbar-nav > .open > a,
259
+.navbar-inverse .navbar-nav > .active > a {
260
+  background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
261
+  background-image:      -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
262
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
263
+  background-image:         linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
264
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
265
+  background-repeat: repeat-x;
266
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
267
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
268
+}
269
+.navbar-inverse .navbar-brand,
270
+.navbar-inverse .navbar-nav > li > a {
271
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
272
+}
273
+.navbar-static-top,
274
+.navbar-fixed-top,
275
+.navbar-fixed-bottom {
276
+  border-radius: 0;
277
+}
278
+.alert {
279
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
280
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
281
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
282
+}
283
+.alert-success {
284
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
285
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
286
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
287
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
288
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
289
+  background-repeat: repeat-x;
290
+  border-color: #b2dba1;
291
+}
292
+.alert-info {
293
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
294
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
295
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
296
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
297
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
298
+  background-repeat: repeat-x;
299
+  border-color: #9acfea;
300
+}
301
+.alert-warning {
302
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
303
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
304
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
305
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
306
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
307
+  background-repeat: repeat-x;
308
+  border-color: #f5e79e;
309
+}
310
+.alert-danger {
311
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
312
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
313
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
314
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
315
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
316
+  background-repeat: repeat-x;
317
+  border-color: #dca7a7;
318
+}
319
+.progress {
320
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
321
+  background-image:      -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
322
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
323
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
324
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
325
+  background-repeat: repeat-x;
326
+}
327
+.progress-bar {
328
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
329
+  background-image:      -o-linear-gradient(top, #428bca 0%, #3071a9 100%);
330
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3071a9));
331
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
332
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
333
+  background-repeat: repeat-x;
334
+}
335
+.progress-bar-success {
336
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
337
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
338
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
339
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
340
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
341
+  background-repeat: repeat-x;
342
+}
343
+.progress-bar-info {
344
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
345
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
346
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
347
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
348
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
349
+  background-repeat: repeat-x;
350
+}
351
+.progress-bar-warning {
352
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
353
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
354
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
355
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
356
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
357
+  background-repeat: repeat-x;
358
+}
359
+.progress-bar-danger {
360
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
361
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
362
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
363
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
364
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
365
+  background-repeat: repeat-x;
366
+}
367
+.progress-bar-striped {
368
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
369
+  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
370
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
371
+}
372
+.list-group {
373
+  border-radius: 4px;
374
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
375
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
376
+}
377
+.list-group-item.active,
378
+.list-group-item.active:hover,
379
+.list-group-item.active:focus {
380
+  text-shadow: 0 -1px 0 #3071a9;
381
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
382
+  background-image:      -o-linear-gradient(top, #428bca 0%, #3278b3 100%);
383
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3278b3));
384
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
385
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
386
+  background-repeat: repeat-x;
387
+  border-color: #3278b3;
388
+}
389
+.list-group-item.active .badge,
390
+.list-group-item.active:hover .badge,
391
+.list-group-item.active:focus .badge {
392
+  text-shadow: none;
393
+}
394
+.panel {
395
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
396
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
397
+}
398
+.panel-default > .panel-heading {
399
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
400
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
401
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
402
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
403
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
404
+  background-repeat: repeat-x;
405
+}
406
+.panel-primary > .panel-heading {
407
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
408
+  background-image:      -o-linear-gradient(top, #428bca 0%, #357ebd 100%);
409
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd));
410
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
411
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
412
+  background-repeat: repeat-x;
413
+}
414
+.panel-success > .panel-heading {
415
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
416
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
417
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
418
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
419
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
420
+  background-repeat: repeat-x;
421
+}
422
+.panel-info > .panel-heading {
423
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
424
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
425
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
426
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
427
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
428
+  background-repeat: repeat-x;
429
+}
430
+.panel-warning > .panel-heading {
431
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
432
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
433
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
434
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
435
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
436
+  background-repeat: repeat-x;
437
+}
438
+.panel-danger > .panel-heading {
439
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
440
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
441
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
442
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
443
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
444
+  background-repeat: repeat-x;
445
+}
446
+.well {
447
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
448
+  background-image:      -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
449
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
450
+  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
451
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
452
+  background-repeat: repeat-x;
453
+  border-color: #dcdcdc;
454
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
455
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
456
+}
457
+/*# sourceMappingURL=bootstrap-theme.css.map */

+ 1
- 0
static/bootstrap/css/bootstrap-theme.css.map
File diff suppressed because it is too large
View File


+ 5
- 0
static/bootstrap/css/bootstrap-theme.min.css
File diff suppressed because it is too large
View File


+ 6358
- 0
static/bootstrap/css/bootstrap.css
File diff suppressed because it is too large
View File


+ 1
- 0
static/bootstrap/css/bootstrap.css.map
File diff suppressed because it is too large
View File


+ 5
- 0
static/bootstrap/css/bootstrap.min.css
File diff suppressed because it is too large
View File


BIN
static/bootstrap/fonts/glyphicons-halflings-regular.eot View File


+ 229
- 0
static/bootstrap/fonts/glyphicons-halflings-regular.svg View File

@@ -0,0 +1,229 @@
1
+<?xml version="1.0" standalone="no"?>
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
3
+<svg xmlns="http://www.w3.org/2000/svg">
4
+<metadata></metadata>
5
+<defs>
6
+<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
7
+<font-face units-per-em="1200" ascent="960" descent="-240" />
8
+<missing-glyph horiz-adv-x="500" />
9
+<glyph />
10
+<glyph />
11
+<glyph unicode="&#xd;" />
12
+<glyph unicode=" " />
13
+<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
14
+<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
15
+<glyph unicode="&#xa0;" />
16
+<glyph unicode="&#x2000;" horiz-adv-x="652" />
17
+<glyph unicode="&#x2001;" horiz-adv-x="1304" />
18
+<glyph unicode="&#x2002;" horiz-adv-x="652" />
19
+<glyph unicode="&#x2003;" horiz-adv-x="1304" />
20
+<glyph unicode="&#x2004;" horiz-adv-x="434" />
21
+<glyph unicode="&#x2005;" horiz-adv-x="326" />
22
+<glyph unicode="&#x2006;" horiz-adv-x="217" />
23
+<glyph unicode="&#x2007;" horiz-adv-x="217" />
24
+<glyph unicode="&#x2008;" horiz-adv-x="163" />
25
+<glyph unicode="&#x2009;" horiz-adv-x="260" />
26
+<glyph unicode="&#x200a;" horiz-adv-x="72" />
27
+<glyph unicode="&#x202f;" horiz-adv-x="260" />
28
+<glyph unicode="&#x205f;" horiz-adv-x="326" />
29
+<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
30
+<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
31
+<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
32
+<glyph unicode="&#x2601;" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
33
+<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
34
+<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
35
+<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
36
+<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
37
+<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
38
+<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
39
+<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
40
+<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
41
+<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
42
+<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
43
+<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
44
+<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
45
+<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
46
+<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
47
+<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
48
+<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
49
+<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
50
+<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
51
+<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
52
+<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
53
+<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
54
+<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
55
+<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
56
+<glyph unicode="&#xe023;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
57
+<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
58
+<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
59
+<glyph unicode="&#xe026;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
60
+<glyph unicode="&#xe027;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
61
+<glyph unicode="&#xe028;" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
62
+<glyph unicode="&#xe029;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
63
+<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
64
+<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
65
+<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
66
+<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
67
+<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
68
+<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
69
+<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
70
+<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
71
+<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
72
+<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
73
+<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
74
+<glyph unicode="&#xe041;" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
75
+<glyph unicode="&#xe042;" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
76
+<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
77
+<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
78
+<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
79
+<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
80
+<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
81
+<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
82
+<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
83
+<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
84
+<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
85
+<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
86
+<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
87
+<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
88
+<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
89
+<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
90
+<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
91
+<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
92
+<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
93