├── application
├── flicket_admin
│ ├── __init__.py
│ ├── forms
│ │ ├── __init__.py
│ │ ├── form_login.py
│ │ └── form_config.py
│ ├── models
│ │ └── __init__.py
│ ├── scripts
│ │ └── __init__.py
│ ├── templates
│ │ ├── admin.html
│ │ ├── flashmessages.html
│ │ ├── admin_menu.html
│ │ ├── admin_email_test.html
│ │ ├── admin_edit_group.html
│ │ ├── admin_delete_group.html
│ │ ├── admin_groups.html
│ │ ├── admin_delete_user.html
│ │ └── admin_config.html
│ └── views
│ │ ├── __init__.py
│ │ └── view_email_test.py
├── flicket
│ ├── templates
│ │ ├── __empty__.html
│ │ ├── flicket_edittopic.html
│ │ ├── email_test.html
│ │ ├── 403.html
│ │ ├── 404.html
│ │ ├── email_footer.html
│ │ ├── flicket_footer.html
│ │ ├── flicket_create.html
│ │ ├── email_ticket_close.html
│ │ ├── email_ticket_assign.html
│ │ ├── flicket_editpost.html
│ │ ├── email_ticket_release.html
│ │ ├── email_password_reset.html
│ │ ├── email_ticket_department_category.html
│ │ ├── flicket_flashmessages.html
│ │ ├── email_ticket_replies.html
│ │ ├── email_ticket_create.html
│ │ ├── email_base.html
│ │ ├── 500.html
│ │ ├── email_include_details.html
│ │ ├── email_ticket_not_closed.html
│ │ ├── flicket_apijson_users.html
│ │ ├── flicket_apijson_department_categories.html
│ │ ├── flicket_password_reset.html
│ │ ├── flicket_tickets_pag.html
│ │ ├── flicket_department_edit.html
│ │ ├── flicket_apijson_statuses.html
│ │ ├── flicket_deletepost.html
│ │ ├── flicket_apijson_departments.html
│ │ ├── flicket_delete.html
│ │ ├── flicket_deletetopic.html
│ │ ├── flicket_category_edit.html
│ │ ├── flicket_user_details.html
│ │ ├── flicket_history.html
│ │ ├── flicket_department_category.html
│ │ ├── flicket_assign.html
│ │ ├── flicket_apijson_categories.html
│ │ ├── flicket_index.html
│ │ ├── flicket_base.html
│ │ ├── flicket_login.html
│ │ ├── flicket_categories.html
│ │ ├── flicket_items.html
│ │ └── flicket_form_reply.html
│ ├── static
│ │ ├── flicket_uploads
│ │ │ └── .gitignore
│ │ ├── icons
│ │ │ ├── favicon.ico
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ └── site.webmanifest
│ │ ├── flicket_avatars
│ │ │ ├── __default_robot.png
│ │ │ ├── __default_profile.png
│ │ │ └── .gitignore
│ │ ├── js
│ │ │ ├── hide_element.js
│ │ │ └── npm.js
│ │ └── svgs
│ │ │ ├── solid
│ │ │ ├── file.svg
│ │ │ ├── trash.svg
│ │ │ ├── long-arrow-alt-down.svg
│ │ │ ├── long-arrow-alt-up.svg
│ │ │ ├── plus.svg
│ │ │ ├── user.svg
│ │ │ ├── bars.svg
│ │ │ ├── times.svg
│ │ │ ├── id-badge.svg
│ │ │ ├── envelope.svg
│ │ │ ├── home.svg
│ │ │ ├── edit.svg
│ │ │ ├── sort-alpha-up.svg
│ │ │ ├── sort-alpha-down.svg
│ │ │ ├── sort-numeric-up.svg
│ │ │ ├── sort-numeric-down.svg
│ │ │ ├── file-csv.svg
│ │ │ └── link.svg
│ │ │ └── brands
│ │ │ └── github.svg
│ ├── __init__.py
│ ├── forms
│ │ ├── __init__.py
│ │ ├── search.py
│ │ └── form_login.py
│ ├── scripts
│ │ ├── __init__.py
│ │ ├── jinja2_functions.py
│ │ ├── hash_password.py
│ │ ├── forms.py
│ │ ├── decorators.py
│ │ ├── subscriptions.py
│ │ ├── upload_choice_generator.py
│ │ ├── flicket_config.py
│ │ ├── flicket_user_details.py
│ │ ├── flicket_functions.py
│ │ ├── functions_login.py
│ │ └── pie_charts.py
│ ├── models
│ │ └── __init__.py
│ └── views
│ │ ├── help.py
│ │ ├── __init__.py
│ │ ├── render_uploads.py
│ │ ├── index.py
│ │ ├── history.py
│ │ ├── claim.py
│ │ ├── users.py
│ │ ├── create.py
│ │ ├── edit_status.py
│ │ ├── release.py
│ │ ├── departments.py
│ │ ├── categories.py
│ │ ├── assign.py
│ │ ├── department_category.py
│ │ ├── user_edit.py
│ │ └── subscribe.py
├── flicket_api
│ ├── __init__.py
│ ├── views
│ │ ├── sphinx_helper.py
│ │ ├── __init__.py
│ │ ├── errors.py
│ │ ├── actions.py
│ │ ├── auth.py
│ │ ├── tokens.py
│ │ └── department_categories.py
│ └── scripts
│ │ └── paginated_api.py
├── translations
│ └── fr
│ │ └── LC_MESSAGES
│ │ └── messages.mo
└── flicket_errors
│ ├── __init__.py
│ └── handlers.py
├── migrations
├── README
├── script.py.mako
├── alembic.ini
├── versions
│ ├── 70820003badd_add_logging_of_hours.py
│ ├── 253ae54f5788_change_category_config_options.py
│ ├── 7ec6c2a6a1c8_add_disabled_column.py
│ ├── bcac6741b320_hours_scale_two_dp.py
│ └── 9e59e0b9d1cf_last_updated.py
└── env.py
├── scripts
├── .gitignore
├── __init__.py
├── login_functions.py
└── password_valdation.py
├── CONTRIBUTORS.md
├── tmp
└── .gitignore
├── docs
├── images
│ ├── 02_tickets.png
│ ├── 03_ticket.png
│ ├── 05_users.png
│ ├── 01_home_page.png
│ ├── 2019-01-22_18_40_21-Admin.png
│ ├── 2019-01-22_18_40_46-Add_User.png
│ ├── 04_create_ticket_markdown_preview.png
│ ├── 2019-01-22_18_43_25-Flicket_Configuration.png
│ └── 04_create_ticket_markdown_preview_2019-11-12_17-17-04.png
├── api.rst
├── index.rst
├── Makefile
├── screenshots.rst
├── make.bat
├── requirements.rst
├── export_import_users.rst
├── languages.rst
├── conf.py
├── admin.rst
├── install.rst
└── faq.rst
├── .flaskenv
├── SECURITY.md
├── requirements-development.txt
├── babel.cfg
├── TODO.md
├── run.wsgi
├── requirements.txt
├── .gitignore
├── .readthedocs.yaml
├── README.rst
├── LICENSE.md
├── alembic.ini
└── config.py
/application/flicket_admin/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/scripts/.gitignore:
--------------------------------------------------------------------------------
1 | ../config.json
2 | config_work.json
3 | ../users.json
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTORS
2 |
3 | * SolvingCurves
4 | * xdml
5 | * juanvmarquezl
6 |
--------------------------------------------------------------------------------
/tmp/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
--------------------------------------------------------------------------------
/application/flicket/templates/__empty__.html:
--------------------------------------------------------------------------------
1 | {{ _('It appears your view request url was incomplete.') }}
--------------------------------------------------------------------------------
/docs/images/02_tickets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/02_tickets.png
--------------------------------------------------------------------------------
/docs/images/03_ticket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/03_ticket.png
--------------------------------------------------------------------------------
/docs/images/05_users.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/05_users.png
--------------------------------------------------------------------------------
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=application
2 | FLASK_RUN_PORT=5000
3 | FLASK_DEBUG=1
4 | TEMPLATES_AUTO_RELOAD=True
5 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | For security concerns you can contact me at the following email address:
2 | evereux@gmail.com
--------------------------------------------------------------------------------
/docs/images/01_home_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/01_home_page.png
--------------------------------------------------------------------------------
/requirements-development.txt:
--------------------------------------------------------------------------------
1 | Sphinx==5.1.1
2 | sphinx-rtd-theme==1.0.0
3 | sphinxcontrib-httpdomain==1.8.0
4 |
--------------------------------------------------------------------------------
/docs/images/2019-01-22_18_40_21-Admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/2019-01-22_18_40_21-Admin.png
--------------------------------------------------------------------------------
/application/flicket/static/flicket_uploads/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
--------------------------------------------------------------------------------
/application/flicket/static/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/icons/favicon.ico
--------------------------------------------------------------------------------
/application/flicket_api/__init__.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-#
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
--------------------------------------------------------------------------------
/docs/images/2019-01-22_18_40_46-Add_User.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/2019-01-22_18_40_46-Add_User.png
--------------------------------------------------------------------------------
/application/flicket/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
--------------------------------------------------------------------------------
/application/flicket/forms/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
--------------------------------------------------------------------------------
/application/flicket/scripts/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
--------------------------------------------------------------------------------
/application/flicket/static/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/application/flicket/static/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/application/translations/fr/LC_MESSAGES/messages.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/translations/fr/LC_MESSAGES/messages.mo
--------------------------------------------------------------------------------
/docs/images/04_create_ticket_markdown_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/04_create_ticket_markdown_preview.png
--------------------------------------------------------------------------------
/application/flicket/static/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/application/flicket_admin/forms/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
--------------------------------------------------------------------------------
/application/flicket_admin/models/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
--------------------------------------------------------------------------------
/application/flicket_admin/scripts/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
--------------------------------------------------------------------------------
/babel.cfg:
--------------------------------------------------------------------------------
1 | [ignore : env/**]
2 |
3 | [python: **.py]
4 | [jinja2: **/templates/**.html]
5 | encoding = utf-8
6 | extensions=jinja2.ext.autoescape,jinja2.ext.with_
7 |
--------------------------------------------------------------------------------
/docs/images/2019-01-22_18_43_25-Flicket_Configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/2019-01-22_18_43_25-Flicket_Configuration.png
--------------------------------------------------------------------------------
/application/flicket/static/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/application/flicket/static/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/application/flicket/static/flicket_avatars/__default_robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/flicket_avatars/__default_robot.png
--------------------------------------------------------------------------------
/application/flicket/static/flicket_avatars/__default_profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/application/flicket/static/flicket_avatars/__default_profile.png
--------------------------------------------------------------------------------
/application/flicket/static/flicket_avatars/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
5 | !__default_profile.png
6 | !__default_robot.png
--------------------------------------------------------------------------------
/docs/images/04_create_ticket_markdown_preview_2019-11-12_17-17-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evereux/flicket/HEAD/docs/images/04_create_ticket_markdown_preview_2019-11-12_17-17-04.png
--------------------------------------------------------------------------------
/application/flicket/models/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from application import db
7 |
8 | Base = db.Model
9 |
--------------------------------------------------------------------------------
/application/flicket_errors/__init__.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import Blueprint
7 |
8 | bp_errors = Blueprint('flicket-errors', __name__)
9 |
--------------------------------------------------------------------------------
/application/flicket_api/views/sphinx_helper.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from application import app
7 |
8 | api_url = "{}".format(app.config['FLICKET_API'])
9 |
--------------------------------------------------------------------------------
/application/flicket/scripts/jinja2_functions.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import datetime
5 |
6 | from flask import render_template
7 |
8 |
9 | def now_year():
10 | return datetime.datetime.now().strftime('%Y')
11 |
--------------------------------------------------------------------------------
/application/flicket_api/views/__init__.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-#
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 |
7 | from flask import Blueprint
8 |
9 | bp_api = Blueprint('bp_api', __name__)
10 |
11 |
12 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # Flicket Todo List
2 |
3 | ## High
4 |
5 | ## Medium
6 | * lock tickets after x number of days? This will have to be done via a Command function run as a crop job?
7 |
8 | ## Low
9 | * add ability to filter tickets by who started them as well as who they're assigned to.
10 |
--------------------------------------------------------------------------------
/application/flicket/static/js/hide_element.js:
--------------------------------------------------------------------------------
1 | function hide_element() {
2 | var x = document.getElementById("flask-pagedown-content-preview");
3 | if (x.style.display === "block") {
4 | x.style.display = "none";
5 | } else {
6 | x.style.display = "block";
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/application/flicket/static/icons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_edittopic.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
{{ title }}
6 |
7 | {% include 'flicket_post_box.html' %}
8 |
9 |
10 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/email_test.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Flicket test email.
6 |
7 |
8 | The flicket email settings appear to be working. Excellent.
9 |
10 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/403.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
{{ _('403 Error - Forbidden') }}
6 |
{{ _('Sorry, you don\'t have permission to access this resource.') }}
7 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/email_footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Flicket {{ g.__version__ }} © {{ now_year() }} |
4 | {{ _('Source code available at:') }} Github .
5 |
6 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "flicket_base.html" %}
3 | {% block content %}
4 |
5 |
6 |
7 | {% include 'admin_menu.html' %}
8 |
9 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Flicket {{ g.__version__ }} © {{ now_year() }} Paul Bourne |
4 | {{ _('Source code available at:') }} Github .
5 |
6 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/long-arrow-alt-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/long-arrow-alt-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/plus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_create.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
{{ title }}
7 | {% include 'flicket_post_box.html' %}
8 |
9 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/email_ticket_close.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 |
8 | {{ _('Ticket has been closed:') }} {{ ticket.id_zfill }}
9 |
10 | {% include('email_include_details.html') %}
11 | {% endblock %}
--------------------------------------------------------------------------------
/run.wsgi:
--------------------------------------------------------------------------------
1 | activate_this = r'C:\python\flicket\env\Scripts\activate_this.py'
2 |
3 | with open(activate_this) as file_:
4 | exec(file_.read(), dict(__file__=activate_this))
5 |
6 | import sys, os
7 | # application environment so apache can load application.
8 | abspath = os.path.dirname(__file__)
9 | sys.path.append(abspath)
10 | os.chdir(abspath)
11 |
12 | from application import app as application
13 |
--------------------------------------------------------------------------------
/application/flicket/scripts/hash_password.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import bcrypt
7 |
8 |
9 | def hash_password(password):
10 | """ Convert input with bcrypt and return """
11 |
12 | password = password.encode('utf-8')
13 | password = bcrypt.hashpw(password, bcrypt.gensalt())
14 |
15 | return password
16 |
--------------------------------------------------------------------------------
/application/flicket/templates/email_ticket_assign.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 |
8 | {{ ticket.assigned.name }} {{ _('has been assigned to ticket:') }} {{ number }}
9 |
10 | {% include('email_include_details.html') %}
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_editpost.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
8 |
9 | {% include('flicket_form_reply.html') %}
10 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/email_ticket_release.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 |
8 | {{ _('Ticket has been released. Ticket is now no longer assigned to anyone. Ticket:') }} {{ number }}
9 |
10 | {% include('email_include_details.html') %}
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/bars.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/scripts/forms.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 |
7 | # used for debugging purposes only
8 | def print_errors(form):
9 | for field, errors in form.errors.items():
10 | for error in errors:
11 | print("Error in the {} field - {}".format(
12 | getattr(form, field).label.text,
13 | error
14 | ))
15 |
--------------------------------------------------------------------------------
/application/flicket/templates/email_password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 |
8 | Your new password is: {{ new_password }}.
9 |
10 |
11 | Once you have logged in please change your password as soon as possible. Your password is not secure until you
12 | do so.
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | bcrypt==4.0.1
2 | jinja2==3.1.4
3 | flask==2.3.3
4 | flask-babel==4.0.0
5 | flask-httpauth==4.8.0
6 | flask-login==0.6.2
7 | flask-markdown==0.3
8 | flask-mail==0.9.1
9 | flask-migrate==4.0.5
10 | flask-pagedown==0.4.0
11 | flask-principal==0.4.0
12 | flask-sqlalchemy==3.1.1
13 | flask-script==2.0.6
14 | flask-wtf==1.2.1
15 | markdown==3.5.1
16 | plotly==5.18.0
17 | pymysql==1.1.1
18 | python-dotenv==0.20.0
19 | sqlalchemy==2.0.23
20 | Werkzeug==3.0.3
21 |
--------------------------------------------------------------------------------
/application/flicket/templates/email_ticket_department_category.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 |
8 | {{ _('Department') }} / {{_('Category') }} "{{ ticket.department_category }}" {{ _('has been assigned to ticket:') }} {{ number }}
9 |
10 | {% include('email_include_details.html') %}
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_flashmessages.html:
--------------------------------------------------------------------------------
1 | {% with messages = get_flashed_messages(with_categories=true) %}
2 | {% if messages %}
3 |
4 |
5 | {% for category, message in messages %}
6 | {{ message }}
7 | {% endfor %}
8 |
9 |
10 | {% endif %}
11 | {% endwith %}
12 |
--------------------------------------------------------------------------------
/application/flicket/templates/email_ticket_replies.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 | {% include('email_include_details.html') %}
8 |
9 | {{ _('There are new replies to ticket:') }} {{ number }} .
10 |
11 |
12 | {{ reply.user.name }} replied:
13 | "{{ reply.content }}"
14 |
15 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/email_ticket_create.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 |
8 | {{ _('You have successfully created a new ticket:') }} {{ number }}
9 |
10 | {% include('email_include_details.html') %}
11 | {{ _('Content') }}
12 |
13 | {{ ticket.content }}
14 |
15 |
16 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/times.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/views/help.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 |
7 | from flask import render_template
8 | from flask_login import login_required
9 |
10 | from . import flicket_bp
11 | from application import app
12 |
13 |
14 | # view users
15 | @flicket_bp.route(app.config['FLICKET'] + 'markdown_primer/', methods=['GET', 'POST'])
16 | @login_required
17 | def markdown_primer():
18 | return render_template('markdown_primer.html')
19 |
--------------------------------------------------------------------------------
/application/flicket_admin/views/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import Blueprint
7 |
8 | import os
9 |
10 | static_folder = os.path.join(os.getcwd(), 'application/home/static')
11 |
12 | admin_bp = Blueprint('admin_bp', __name__,
13 | template_folder="../templates",
14 | static_folder=static_folder,
15 | static_url_path='/home/static',
16 | )
17 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/id-badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/views/__init__.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import Blueprint
7 |
8 | import os
9 |
10 | static_folder = os.path.join(os.getcwd(), 'application/flicket/static')
11 |
12 | flicket_bp = Blueprint('flicket_bp', __name__,
13 | template_folder="../templates",
14 | static_folder=static_folder,
15 | static_url_path='/flicket/static',
16 | )
17 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/flashmessages.html:
--------------------------------------------------------------------------------
1 | {% with messages = get_flashed_messages(with_categories=true) %}
2 | {% if messages %}
3 |
4 |
5 |
6 | {% for category, message in messages %}
7 | {{ message }}
8 | {% endfor %}
9 |
10 |
11 | {% endif %}
12 | {% endwith %}
13 |
--------------------------------------------------------------------------------
/application/flicket/static/js/npm.js:
--------------------------------------------------------------------------------
1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
2 | require('../../js/transition.js')
3 | require('../../js/alert.js')
4 | require('../../js/button.js')
5 | require('../../js/carousel.js')
6 | require('../../js/collapse.js')
7 | require('../../js/dropdown.js')
8 | require('../../js/modal.js')
9 | require('../../js/tooltip.js')
10 | require('../../js/popover.js')
11 | require('../../js/scrollspy.js')
12 | require('../../js/tab.js')
13 | require('../../js/affix.js')
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/envelope.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/templates/email_base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
18 |
19 |
20 | {% block content %}{% endblock %}
21 |
22 | {% include 'email_footer.html' %}
23 |
24 |
--------------------------------------------------------------------------------
/application/flicket/scripts/decorators.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from threading import Thread
7 |
8 |
9 | def send_async_email(f):
10 | """
11 | Threading function blatantly copied from https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xi
12 | -email-support
13 | :param f:
14 | :return:
15 | """
16 |
17 | def wrapper(*args, **kwargs):
18 | thr = Thread(target=f, args=args, kwargs=kwargs)
19 | thr.start()
20 |
21 | return wrapper
22 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket_api/views/errors.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import jsonify
7 | from werkzeug.http import HTTP_STATUS_CODES
8 |
9 |
10 | def error_response(status_code, message=None):
11 | payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
12 | if message:
13 | payload['message'] = message
14 | response = jsonify(payload)
15 | response.status_code = status_code
16 | return response
17 |
18 |
19 | def bad_request(message):
20 | return error_response(400, message)
21 |
--------------------------------------------------------------------------------
/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/application/flicket/views/render_uploads.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import os
7 | from flask import send_from_directory
8 | from flask_login import login_required
9 |
10 | from . import flicket_bp
11 | from application import app
12 |
13 |
14 | # return images
15 | @flicket_bp.route(app.config['WEBHOME'] + 'flicket_uploads/', methods=['GET', 'POST'])
16 | @login_required
17 | def view_ticket_uploads(filename):
18 | path = os.path.join(os.getcwd(), app.config['ticket_upload_folder'])
19 | return send_from_directory(path, filename)
20 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | API
2 | ===
3 |
4 | .. automodule:: flicket_api.views.tokens
5 | :members:
6 |
7 | .. automodule:: flicket_api.views.users
8 | :members:
9 |
10 | .. automodule:: flicket_api.views.tickets
11 | :members:
12 |
13 | .. automodule:: flicket_api.views.posts
14 | :members:
15 |
16 | .. automodule:: flicket_api.views.departments
17 | :members:
18 |
19 | .. automodule:: flicket_api.views.priorities
20 | :members:
21 |
22 | .. automodule:: flicket_api.views.status
23 | :members:
24 |
25 | .. automodule:: flicket_api.views.subscriptions
26 | :members:
27 |
28 | .. automodule:: flicket_api.views.uploads
29 | :members:
30 |
31 |
32 |
--------------------------------------------------------------------------------
/application/flicket/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
{{ _('An unexpected error has occurred') }}
6 |
7 | {{ _('Please report this error to your administrator. Please provide the following details:') }}
8 |
9 |
10 | {{ _('Page you were navigating.') }}
11 | {{ _('Time you saw this error.') }}
12 | {{ _('Any other information that may help reproduce the error.') }}
13 |
14 |
{{ _('Back') }}
15 |
16 | {% endblock %}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | .idea/
7 | .vscode/
8 |
9 | __archive__
10 | __reference__
11 | __other__
12 | config.json
13 | users.json
14 | migrations_laptop
15 | migrations_desktop
16 | test.db
17 | test.py
18 |
19 | env*/
20 | venv*/
21 | .directory
22 | .cache/v/cache/lastfailed
23 |
24 | *sublime*
25 |
26 | docs/_build/*
27 |
28 | # uwsgi files for server.
29 | flicket.ini
30 | flicket_uswgi.wsgi
31 |
32 | # vim
33 | tags
34 |
35 | # script currently has a memory leak.
36 | temp_populate_database_with_junk.py
37 |
38 | # don't store default sqlite db
39 | flicket.db
40 |
41 | # don't store backup config
42 | config_old.json
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Flicket Documentation
2 | ======================
3 |
4 | Flicket is a simple web based ticketing system written in Python using
5 | the flask web framework which supports English and French locales.
6 |
7 |
8 | Why Flicket?
9 | ---------------
10 | I could not find a simple open source ticketing system that I really liked.
11 | So, decided to have a crack at creating something written in Python.
12 |
13 | .. toctree::
14 | :maxdepth: 2
15 | :caption: Contents:
16 |
17 | requirements
18 | install
19 | admin
20 | export_import_users
21 | languages
22 | faq
23 | screenshots
24 |
25 | .. toctree::
26 | :maxdepth: 3
27 | :caption: Programmer Reference:
28 |
29 | api
30 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/sort-alpha-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.11"
12 |
13 | # Build documentation in the "docs/" directory with Sphinx
14 | sphinx:
15 | configuration: docs/conf.py
16 |
17 | # Optionally build your docs in additional formats such as PDF and ePub
18 | # formats:
19 | # - pdf
20 | # - epub
21 |
22 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
23 | python:
24 | install:
25 | - requirements: requirements-development.txt
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/sort-alpha-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/sort-numeric-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/sort-numeric-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/templates/email_include_details.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ _('Status') }}
4 | {{ ticket.current_status.status }}
5 |
6 |
7 | {{ _('Priority') }}
8 | {{ ticket.ticket_priority.priority }}
9 |
10 |
11 | {{ _('Assigned') }}
12 | {% if ticket.assigned.username %}{{ ticket.assigned.name }}{% else %}ticket not claimed{% endif %}
13 |
14 |
15 | {{ _('Content') }}
16 | {{ ticket.content }}
17 |
18 |
--------------------------------------------------------------------------------
/application/flicket/scripts/subscriptions.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 |
7 | from application import db
8 | from application.flicket.models.flicket_models import FlicketSubscription
9 | from application.flicket.scripts.flicket_functions import add_action
10 |
11 |
12 | def subscribe_user(ticket, user):
13 | if not ticket.is_subscribed(user):
14 | # subscribe user to ticket
15 | # noinspection PyArgumentList
16 | subscribe = FlicketSubscription(user=user, ticket=ticket)
17 | add_action(ticket, 'subscribe', recipient=user)
18 | db.session.add(subscribe)
19 | db.session.commit()
20 | return True
21 |
22 | return False
23 |
--------------------------------------------------------------------------------
/docs/screenshots.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | Screenshots
3 | ===========
4 |
5 | Home Page
6 | ---------
7 |
8 | .. image:: images/01_home_page.png
9 |
10 |
11 | Tickets
12 | -------
13 |
14 | All tickets.
15 |
16 | .. image:: images/02_tickets.png
17 |
18 | View Ticket
19 | -----------
20 |
21 | .. image:: images/03_ticket.png
22 |
23 |
24 | Create Ticket
25 | -------------
26 |
27 | .. image:: images/04_create_ticket_markdown_preview.png
28 |
29 | Users
30 | -----
31 |
32 | .. image:: images/05_users.png
33 |
34 | Admin Panel
35 | -----------
36 |
37 | .. image:: images/2019-01-22_18_40_21-Admin.png
38 |
39 | Admin Panel - Add User
40 | ----------------------
41 |
42 | .. image:: images/2019-01-22_18_40_46-Add_User.png
43 |
44 | Admin Panel - Configuration
45 | ---------------------------
46 |
47 | .. image:: images/2019-01-22_18_43_25-Flicket_Configuration.png
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/requirements.rst:
--------------------------------------------------------------------------------
1 | .. _requirements:
2 |
3 | Requirements
4 | ============
5 |
6 | Operating System
7 | ----------------
8 |
9 | This will run on either Linux or Windows. Mac is untested.
10 |
11 |
12 | Python
13 | ------
14 | Python =>3.9.
15 |
16 |
17 |
18 | SQL Database Server
19 | -------------------
20 |
21 | Out of the box Flicket is configured to work with `MySQL `_. But there
22 | should be no reason other SQLAlchemy supported databases won't work
23 | just as well.
24 |
25 | .. note::
26 |
27 | When I last tried SQLite I had problems configuring the email settings
28 | within the administration settings. You may have to change them manually
29 | within SQLite.
30 |
31 |
32 | Web Server
33 | ----------
34 |
35 | For a production environment a webserver such as `Apache `_
36 | or `nginx `_ should be used to serve the application.
37 |
--------------------------------------------------------------------------------
/application/flicket_errors/handlers.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import render_template, request
7 |
8 | from application import db
9 | from application.flicket_errors import bp_errors
10 | from application.flicket_api.views.errors import error_response as api_error_response
11 |
12 |
13 | def wants_json_response():
14 | return request.accept_mimetypes['application/json'] >= request.accept_mimetypes['text/html']
15 |
16 |
17 | @bp_errors.app_errorhandler(404)
18 | def not_found_error(error):
19 | if wants_json_response():
20 | return api_error_response(404)
21 | return render_template('404.html'), 404
22 |
23 |
24 | @bp_errors.app_errorhandler(500)
25 | def internal_error(error):
26 | db.session.rollback()
27 | if wants_json_response():
28 | return api_error_response(500)
29 | return render_template('500.html'), 500
30 |
--------------------------------------------------------------------------------
/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/migrations/versions/70820003badd_add_logging_of_hours.py:
--------------------------------------------------------------------------------
1 | """add logging of hours
2 |
3 | Revision ID: 70820003badd
4 | Revises: 253ae54f5788
5 | Create Date: 2019-11-19 12:27:42.182034
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '70820003badd'
14 | down_revision = '253ae54f5788'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('flicket_post', sa.Column('hours', sa.Numeric(), server_default='0', nullable=True))
22 | op.add_column('flicket_topic', sa.Column('hours', sa.Numeric(), server_default='0', nullable=True))
23 | # ### end Alembic commands ###
24 |
25 |
26 | def downgrade():
27 | # ### commands auto generated by Alembic - please adjust! ###
28 | op.drop_column('flicket_topic', 'hours')
29 | op.drop_column('flicket_post', 'hours')
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/docs/export_import_users.rst:
--------------------------------------------------------------------------------
1 | Exporting / Importing Flicket Users
2 | -------------------------------------
3 | Exporting
4 | ~~~~~~~~~
5 | If you need to export the users from the Flicket database you can run the
6 | following command:
7 |
8 | flask export-users-to-json
9 |
10 |
11 | This will output a json file in the same folder called `users.json` formatted thus:
12 |
13 | .. code-block:: python
14 |
15 | [
16 | {
17 | "username": "jblogs",
18 | "name": "Joe Blogs",
19 | "email": "jblogs@email.com',
20 | "password": "bcrypt_encoded_string"
21 | }
22 | ]
23 |
24 | To get the bcrypt encoded string of the password you can use the function `hash_password` in
25 | `application.flicket.scripts.hash_password`.
26 |
27 | Importing
28 | ~~~~~~~~~
29 | If you need to import users run the following command:
30 |
31 | flask import-users-from-json
32 |
33 | The file has to formatted as shown in the Exporting example and the filename shall be `users.json`.
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Flicket
2 | =======
3 |
4 | Flicket is a simple web based ticketing system written in Python using
5 | the flask web framework which supports English and French locales. Additional
6 | locales can be added by following the section `Adding Additional Languages`
7 | within this README.
8 |
9 |
10 | Documentation
11 | -------------
12 |
13 | For documentation and screenshots please visit: https://flicket.readthedocs.io/en/latest/
14 |
15 |
16 | Upgrading From Earlier Versions
17 | -------------------------------
18 |
19 | See the changelog for changes and additional steps to take when upgrading.
20 |
21 |
22 | Requirements
23 | ------------
24 | Prior to installing and running Flicket please read these requirements.
25 |
26 | * Python =>3.90
27 |
28 | * SQL Database server with JSON support (for example PostgreSQL >=9.2,
29 | MySQL >=5.7, MariaDB >=10.2, SQLite >=3.9)
30 |
31 |
32 | Production Environment
33 | ----------------------
34 |
35 | To serve Flicket within a production environment webservers such as Apache
36 | or nginx are typically used.
--------------------------------------------------------------------------------
/application/flicket/templates/email_ticket_not_closed.html:
--------------------------------------------------------------------------------
1 | {% extends "email_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ title }}
6 |
7 |
8 | The following tickets that you have created or been assigned have not yet been closed.
9 |
10 |
11 |
12 | Ticket Number
13 | Title
14 | Priority
15 | Status
16 | Assigned
17 |
18 | {% for ticket in tickets %}
19 |
20 | {{ ticket.id_zfill }}
21 | {{ ticket.title }}
22 | {{ ticket.ticket_priority.priority }}
23 | {{ ticket.current_status.status }}
24 | {{ ticket.assigned.name }}
25 |
26 | {% endfor %}
27 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/application/flicket/scripts/upload_choice_generator.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import url_for
7 |
8 | from application.flicket.models.flicket_models import FlicketTicket, FlicketPost
9 |
10 |
11 | def generate_choices(item, id=id):
12 |
13 | query = None
14 |
15 | if item == 'Ticket':
16 | query = FlicketTicket.query.filter_by(id=id).first()
17 | elif item == 'Post':
18 | query = FlicketPost.query.filter_by(id=id).first()
19 |
20 | if query:
21 |
22 | # define the multi select box for document uploads
23 | upload = []
24 | for u in query.uploads:
25 | upload.append((u.id, u.filename, u.original_filename))
26 |
27 | uploads = []
28 |
29 | for x in upload:
30 | uri = url_for('flicket_bp.view_ticket_uploads', filename=x[1])
31 | uri_label = '' + x[2] + ' '
32 | uploads.append((x[0], uri_label))
33 |
34 | return uploads
35 |
--------------------------------------------------------------------------------
/application/flicket/views/index.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import render_template
7 | from flask_login import login_required
8 |
9 | from . import flicket_bp
10 | from application import app
11 | from application.flicket.scripts.pie_charts import create_pie_chart_dict
12 | from application.flicket.models.flicket_models import FlicketTicket
13 |
14 |
15 | # view users
16 | @flicket_bp.route(app.config['FLICKET'], methods=['GET', 'POST'])
17 | @login_required
18 | def index():
19 | """ View showing flicket main page. We use this to display some statistics."""
20 | days = 7
21 |
22 | # CAROUSEL
23 | tickets = FlicketTicket.carousel_query()
24 |
25 | # PIE CHARTS
26 | ids, graph_json = create_pie_chart_dict()
27 |
28 | return render_template('flicket_index.html',
29 | days=days,
30 | tickets=tickets,
31 | ids=ids,
32 | graph_json=graph_json)
33 |
--------------------------------------------------------------------------------
/migrations/versions/253ae54f5788_change_category_config_options.py:
--------------------------------------------------------------------------------
1 | """change category config options
2 |
3 | Revision ID: 253ae54f5788
4 | Revises: 36c91aa9b3b5
5 | Create Date: 2019-11-16 16:58:11.287152
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '253ae54f5788'
14 | down_revision = '36c91aa9b3b5'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('flicket_config', sa.Column('change_category', sa.BOOLEAN(), nullable=True))
22 | op.add_column('flicket_config', sa.Column('change_category_only_admin_or_super_user', sa.BOOLEAN(), nullable=True))
23 | # ### end Alembic commands ###
24 |
25 |
26 | def downgrade():
27 | # ### commands auto generated by Alembic - please adjust! ###
28 | op.drop_column('flicket_config', 'change_category_only_admin_or_super_user')
29 | op.drop_column('flicket_config', 'change_category')
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a
2 | copy of this software and associated documentation files (the
3 | "Software"), to deal in the Software without restriction, including
4 | without limitation the rights to use, copy, modify, merge, publish,
5 | distribute, sublicense, and/or sell copies of the Software, and to
6 | permit persons to whom the Software is furnished to do so, subject to
7 | the following conditions:
8 |
9 | The above copyright notice and this permission notice shall be included
10 | in all copies or substantial portions of the Software.
11 |
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
13 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
15 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
16 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/application/flicket_api/views/actions.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import jsonify, request
7 |
8 | from .sphinx_helper import api_url
9 | from . import bp_api
10 | from application import app
11 | from application.flicket.models.flicket_models import FlicketAction
12 | from application.flicket_api.views.auth import token_auth
13 |
14 |
15 | @bp_api.route(api_url + 'action/', methods=['GET'])
16 | @token_auth.login_required
17 | def get_action(id):
18 | return jsonify(FlicketAction.query.get_or_404(id).to_dict())
19 |
20 |
21 | @bp_api.route(api_url + 'actions/', methods=['GET'])
22 | @token_auth.login_required
23 | def get_actions(ticket_id):
24 | actions = FlicketAction.query.filter_by(ticket_id=ticket_id)
25 | page = request.args.get('page', 1, type=int)
26 | per_page = min(request.args.get('per_page', app.config['posts_per_page'], type=int), 100)
27 | data = FlicketAction.to_collection_dict(actions, page, per_page, 'bp_api.get_actions', ticket_id=ticket_id)
28 | return jsonify(data)
29 |
--------------------------------------------------------------------------------
/application/flicket_api/views/auth.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 |
7 | from flask import g
8 | from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
9 |
10 | from application.flicket.models.flicket_user import FlicketUser
11 | from application.flicket_api.views.errors import error_response
12 |
13 | basic_auth = HTTPBasicAuth()
14 | token_auth = HTTPTokenAuth()
15 |
16 |
17 | @basic_auth.verify_password
18 | def verify_password(username, password):
19 | user = FlicketUser.query.filter_by(username=username).first()
20 | if user is None:
21 | return False
22 | g.current_user = user
23 | return user.check_password(password)
24 |
25 |
26 | @basic_auth.error_handler
27 | def basic_auth_error():
28 | return error_response(401)
29 |
30 |
31 | @token_auth.verify_token
32 | def verify_token(token):
33 | g.current_user = FlicketUser.check_token(token) if token else None
34 | return g.current_user is not None
35 |
36 |
37 | @token_auth.error_handler
38 | def token_auth_error():
39 | return error_response(401)
40 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/file-csv.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_apijson_users.html:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
--------------------------------------------------------------------------------
/application/flicket/scripts/flicket_config.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from application import app
7 | from application.flicket_admin.models.flicket_config import FlicketConfig
8 |
9 |
10 | def set_flicket_config():
11 | """
12 | Updates the flicket application settings based on the values stored in the database.
13 | :return:
14 | """
15 | config = FlicketConfig.query.first()
16 |
17 | app.config.update(
18 | posts_per_page=config.posts_per_page,
19 | allowed_extensions=config.allowed_extensions.split(', '),
20 | ticket_upload_folder=config.ticket_upload_folder,
21 | avatar_upload_folder=config.avatar_upload_folder,
22 | base_url=config.base_url,
23 | application_title=config.application_title,
24 | use_auth_domain=config.use_auth_domain,
25 | auth_domain=config.auth_domain,
26 | csv_dump_limit=config.csv_dump_limit,
27 | change_category=config.change_category,
28 | change_category_only_admin_or_super_user=config.change_category_only_admin_or_super_user,
29 | )
30 |
--------------------------------------------------------------------------------
/application/flicket/scripts/flicket_user_details.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from application.flicket.models.flicket_models import FlicketTicket, FlicketPost
7 |
8 |
9 | class FlicketUserDetails:
10 | """
11 | class returns various details about user from user object input.
12 | """
13 |
14 | def __init__(self, user_obj):
15 | self.user = user_obj
16 | self.id = user_obj.id
17 |
18 | @property
19 | def num_assigned(self):
20 | """ return number of tickers assigned to user """
21 | return FlicketTicket.query.filter_by(assigned=self.user).count()
22 |
23 | @property
24 | def num_posts(self):
25 | """ return number of post made by user """
26 |
27 | return FlicketTicket.query.filter_by(started_id=self.id).count() + FlicketPost.query.filter_by(
28 | user_id=self.id).count()
29 |
30 | def __repr__(self):
31 | return "".format(self.id, self.num_assigned,
32 | self.num_posts)
33 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_apijson_department_categories.html:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
--------------------------------------------------------------------------------
/scripts/login_functions.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 |
6 |
7 | def nt_log_on(domain, username, password):
8 | """
9 |
10 | This feature is experimental for windows hosts that want to authenticate on the
11 | local machines domain running this application.
12 |
13 | # todo: This will eventually be changed to use ldap but I don't currently have a means to test this.
14 | :param domain:
15 | :param username:
16 | :param password:
17 | :return:
18 | """
19 |
20 | valid_os = False
21 | authenticated = False
22 |
23 | if os.name == 'nt':
24 | try:
25 | import pywintypes
26 | import win32security
27 | valid_os = True
28 | except ModuleNotFoundError:
29 | raise ModuleNotFoundError('Is pywin32 installed?')
30 |
31 | if valid_os:
32 |
33 | try:
34 | token = win32security.LogonUser(
35 | username,
36 | domain,
37 | password,
38 | win32security.LOGON32_LOGON_NETWORK,
39 | win32security.LOGON32_PROVIDER_DEFAULT)
40 | authenticated = bool(token)
41 | except pywintypes.error:
42 | pass
43 |
44 | return authenticated
45 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin_menu.html:
--------------------------------------------------------------------------------
1 |
2 | {{ _('Administration') }}
3 |
4 |
31 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
32 | {% endblock %}
--------------------------------------------------------------------------------
/migrations/versions/7ec6c2a6a1c8_add_disabled_column.py:
--------------------------------------------------------------------------------
1 | """add disabled column
2 |
3 | Revision ID: 7ec6c2a6a1c8
4 | Revises: 9e59e0b9d1cf
5 | Create Date: 2020-02-28 16:12:35.548279
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | from sqlalchemy.dialects import mysql
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '7ec6c2a6a1c8'
14 | down_revision = '9e59e0b9d1cf'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('flicket_users', sa.Column('disabled', sa.Boolean(), nullable=True))
22 | # ### end Alembic commands ###
23 |
24 | # update the user column so all values are disabled values are False for
25 | # user.
26 | from application import db
27 | from application.flicket.models.flicket_user import FlicketUser
28 |
29 | users = FlicketUser.query.all()
30 |
31 | if users:
32 | for user in users:
33 | if user.disabled is None:
34 | user.disabled = False
35 |
36 | db.session.commit()
37 |
38 |
39 | def downgrade():
40 | # ### commands auto generated by Alembic - please adjust! ###
41 | op.drop_column('flicket_users', 'disabled')
42 | # ### end Alembic commands ###
43 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/solid/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket_api/scripts/paginated_api.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import url_for
7 |
8 | from application import app
9 |
10 |
11 | class PaginatedAPIMixin(object):
12 |
13 | @staticmethod
14 | def to_collection_dict(query, page, per_page, endpoint, **kwargs):
15 | resources = query.paginate(page=page, per_page=per_page)
16 | data = {
17 | 'items': [item.to_dict() for item in resources.items],
18 | '_meta': {
19 | 'page': page,
20 | 'per_page': per_page,
21 | 'total_pages': resources.pages,
22 | 'total_items': resources.total,
23 | },
24 | '_links': {
25 | 'self': app.config['base_url'] + url_for(endpoint, page=page, per_page=per_page, **kwargs),
26 | 'next': app.config['base_url'] + url_for(endpoint, page=page + 1, per_page=per_page,
27 | **kwargs) if resources.has_next else None,
28 | 'prev': app.config['base_url'] + url_for(endpoint, page=page - 1, per_page=per_page,
29 | **kwargs) if resources.has_prev else None,
30 | },
31 | }
32 |
33 | return data
34 |
--------------------------------------------------------------------------------
/application/flicket/static/svgs/brands/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/languages.rst:
--------------------------------------------------------------------------------
1 | Adding Additional Languages
2 | ---------------------------
3 |
4 | Flicket now supports additional languages through the use of Flask Babel.
5 | To add an additional local:
6 |
7 | * Edit `SUPPORTED_LANGUAGES` in `config.py` and add an additional entry to
8 | the dictionary. For example: `{'en': 'English', 'fr': 'Francais',
9 | 'de': 'German'}`
10 |
11 |
12 | * Whilst in the project root directory you now need to initialise
13 | the new language to generate a template file for it.
14 |
15 | .. code-block::
16 |
17 | pybabel init -i messages.pot -d application/translations -l de
18 |
19 |
20 | * In the folder `application/translations` there should now be a new folder
21 | `de`.
22 |
23 |
24 | * Edit the file `messages.po` in that folder. For example:
25 |
26 | .. code-block::
27 |
28 | msgid "403 Error - Forbidden"
29 | msgstr "403 Error - Verboten"
30 |
31 |
32 | * Compile the translations for use:
33 |
34 | .. code-block::
35 |
36 | pybabel compile -d application/translations
37 |
38 |
39 | * If any python or html text strings have been newly tagged for translation
40 | run:
41 |
42 | .. code-block::
43 |
44 | pybabel extract -F babel.cfg -o messages.pot .
45 |
46 |
47 | * To get the new translations added to the .po files:
48 |
49 | .. code-block::
50 |
51 | pybabel update -i messages.pot -d application/translations
52 |
--------------------------------------------------------------------------------
/application/flicket_admin/forms/form_login.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import bcrypt
7 | from flask_babel import lazy_gettext
8 | from flask_wtf import FlaskForm
9 | from wtforms import BooleanField
10 | from wtforms import PasswordField
11 | from wtforms import StringField
12 | from wtforms.validators import DataRequired
13 |
14 | from application.flicket.models.flicket_user import FlicketUser
15 |
16 |
17 | def login_user_exist(form, field):
18 | """
19 | Ensure the username exists.
20 | :param form:
21 | :param field:
22 | :return True False:
23 | """
24 | result = FlicketUser.query.filter_by(username=form.username.data)
25 | if result.count() == 0:
26 | field.errors.append('Invalid username.')
27 | return False
28 | result = result.first()
29 | if bcrypt.hashpw(form.password.data.encode('utf-8'), result.password) != result.password:
30 | field.errors.append('Invalid password.')
31 | return False
32 |
33 | return True
34 |
35 |
36 | class LogInForm(FlaskForm):
37 | """ Log in form. """
38 | username = StringField(lazy_gettext('username'), validators=[DataRequired(), login_user_exist])
39 | password = PasswordField(lazy_gettext('password'), validators=[DataRequired()])
40 | remember_me = BooleanField(lazy_gettext('remember_me'), default=False)
41 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_tickets_pag.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
34 |
35 |
--------------------------------------------------------------------------------
/application/flicket_admin/views/view_email_test.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import flash
7 | from flask import Markup
8 | from flask import url_for
9 | from flask import render_template
10 | from flask_babel import gettext
11 | from flask_login import login_required
12 |
13 | from application import app
14 | from application.flicket_admin.forms.form_config import EmailTest
15 | from application.flicket.scripts.email import FlicketMail
16 |
17 | from . import admin_bp
18 | from .view_admin import admin_permission
19 |
20 |
21 | # Configuration view
22 | @admin_bp.route(app.config['ADMINHOME'] + 'test_email/', methods=['GET', 'POST'])
23 | @login_required
24 | @admin_permission.require(http_exception=403)
25 | def email_test():
26 | form = EmailTest()
27 |
28 | if form.validate_on_submit():
29 | # send email notification
30 | mail = FlicketMail()
31 | mail.test_email([form.email_address.data])
32 | flash(Markup(gettext(
33 | 'Flicket has tried to send an email to the address you entered. Please check your inbox. If no email has '
34 | 'arrived please double check the config '
35 | ' settings.'.format(app.config["base_url"]))),
36 | category='warning')
37 |
38 | return render_template('admin_email_test.html',
39 | title='Send Email Test',
40 | form=form)
41 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_department_edit.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
{{ title }}
9 |
10 |
11 |
12 |
38 |
39 |
40 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/views/history.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import render_template
7 | from flask_babel import gettext
8 | from flask_login import login_required
9 |
10 | from application import app, flicket_bp
11 | from application.flicket.models.flicket_models import FlicketHistory, FlicketPost, FlicketTicket
12 |
13 |
14 | @flicket_bp.route(app.config['FLICKET'] + 'history/topic//', methods=['GET', 'POST'])
15 | @login_required
16 | def flicket_history_topic(topic_id):
17 |
18 | history = FlicketHistory.query.filter_by(topic_id=topic_id).all()
19 | ticket = FlicketTicket.query.filter_by(id=topic_id).one()
20 |
21 | title = gettext('History')
22 |
23 | return render_template(
24 | 'flicket_history.html',
25 | title=title,
26 | history=history,
27 | ticket=ticket)
28 |
29 |
30 | @flicket_bp.route(app.config['FLICKET'] + 'history/post//', methods=['GET', 'POST'])
31 | @login_required
32 | def flicket_history_post(post_id):
33 |
34 | history = FlicketHistory.query.filter_by(post_id=post_id).all()
35 |
36 | # get the ticket object so we can generate a url to link back to topic.
37 | post = FlicketPost.query.filter_by(id=post_id).one()
38 | ticket = FlicketTicket.query.filter_by(id=post.ticket_id).one()
39 |
40 | title = gettext('History')
41 |
42 | return render_template(
43 | 'flicket_history.html',
44 | title=title,
45 | history=history,
46 | ticket=ticket)
47 |
--------------------------------------------------------------------------------
/application/flicket/scripts/flicket_functions.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import datetime
7 |
8 | from flask import flash, g
9 |
10 | from application import db
11 | from application.flicket.models.flicket_models import FlicketAction
12 |
13 |
14 | def add_action(ticket, action, data=None, recipient=None):
15 | """
16 | :param ticket: ticket object
17 | :param action: string
18 | :param data: dictionary
19 | :param recipient: user object
20 | :return:
21 | """
22 | post_id = None
23 | if ticket.posts:
24 | post_id = ticket.posts[-1].id
25 |
26 | new_action = FlicketAction(
27 | ticket=ticket,
28 | post_id=post_id,
29 | action=action,
30 | data=data,
31 | user=g.user,
32 | recipient=recipient,
33 | date=datetime.datetime.now()
34 | )
35 | db.session.add(new_action)
36 | db.session.commit()
37 |
38 |
39 | def is_ticket_closed(status):
40 | # check to see if topic is closed. ticket can't be edited once it's closed.
41 | if status == 'Closed':
42 | flash('Users can not edit closed tickets.', category='danger')
43 | return True
44 |
45 |
46 | def block_quoter(foo):
47 | """
48 | Indents input with '> '. Used for quoting text in posts.
49 | :param foo:
50 | :return:
51 | """
52 |
53 | foo = foo.strip()
54 | split_string = foo.split('\n')
55 | new_string = ''
56 | if len(split_string) > 0:
57 | for i in split_string:
58 | temp_string = '> ' + i
59 | new_string += temp_string
60 | return new_string
61 | else:
62 | return '> ' + foo
63 |
--------------------------------------------------------------------------------
/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # max length of characters to apply to the
11 | # "slug" field
12 | #truncate_slug_length = 40
13 |
14 | # set to 'true' to run the environment during
15 | # the 'revision' command, regardless of autogenerate
16 | # revision_environment = false
17 |
18 | # set to 'true' to allow .pyc and .pyo files without
19 | # a source .py file to be detected as revisions in the
20 | # versions/ directory
21 | # sourceless = false
22 |
23 | # version location specification; this defaults
24 | # to alembic/versions. When using multiple version
25 | # directories, initial revisions must be specified with --version-path
26 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions
27 |
28 | # the output encoding used when revision files
29 | # are written from script.py.mako
30 | # output_encoding = utf-8
31 |
32 |
33 | sqlalchemy.url = sqlite:///flask_template.db
34 |
35 |
36 | # Logging configuration
37 | [loggers]
38 | keys = root,sqlalchemy,alembic
39 |
40 | [handlers]
41 | keys = console
42 |
43 | [formatters]
44 | keys = generic
45 |
46 | [logger_root]
47 | level = WARN
48 | handlers = console
49 | qualname =
50 |
51 | [logger_sqlalchemy]
52 | level = WARN
53 | handlers =
54 | qualname = sqlalchemy.engine
55 |
56 | [logger_alembic]
57 | level = INFO
58 | handlers =
59 | qualname = alembic
60 |
61 | [handler_console]
62 | class = StreamHandler
63 | args = (sys.stderr,)
64 | level = NOTSET
65 | formatter = generic
66 |
67 | [formatter_generic]
68 | format = %(levelname)-5.5s [%(name)s] %(message)s
69 | datefmt = %H:%M:%S
70 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_apijson_statuses.html:
--------------------------------------------------------------------------------
1 |
42 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_deletepost.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
{{ title }}
12 |
13 |
14 |
15 |
post_id: {{ post.id }} | content: {{ post.content }}
16 |
17 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin_email_test.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "flicket_base.html" %}
3 | {% block content %}
4 |
5 |
6 | {% include 'admin_menu.html' %}
7 |
8 |
9 |
10 |
{{ title }}
11 |
12 |
13 |
34 |
35 |
36 |
37 | {% if form.email_address.errors %}
38 |
39 | {% for error in form.email_address.errors %}
40 | {{ error }}
41 | {% endfor %}
42 |
43 | {% endif %}
44 |
45 |
46 |
47 |
48 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_apijson_departments.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_delete.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
{{ title }}
9 |
10 |
11 |
12 |
40 |
41 | {% endblock %}
--------------------------------------------------------------------------------
/scripts/password_valdation.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import string
7 |
8 | password_length = 8
9 |
10 |
11 | class PasswordStrength:
12 |
13 | def __init__(self, password, special_characters=False):
14 | """
15 | Checks validity of password.
16 | :param special_characters:
17 | :return:
18 | """
19 | self.password = password
20 | # currently not supported
21 | # todo: added special characters requirements
22 | self.special_characters = special_characters
23 |
24 | def is_valid(self):
25 | minimum_length = False
26 | has_digits = False
27 | has_uppercase = False
28 | has_lowercase = False
29 |
30 | if password_length >= password_length:
31 | minimum_length = True
32 |
33 | for digit in string.digits:
34 | if digit in self.password:
35 | has_digits = True
36 |
37 | for char in string.ascii_uppercase:
38 | if char in self.password:
39 | has_uppercase = True
40 |
41 | for char in string.ascii_lowercase:
42 | if char in self.password:
43 | has_lowercase = True
44 |
45 | if all([minimum_length, has_digits, has_uppercase, has_lowercase]):
46 | return True
47 | else:
48 | return False
49 |
50 | @staticmethod
51 | def message_rules():
52 | return ("Password must: \n"
53 | " * be a minimum of {} characters long.\n"
54 | " * contain numbers and letters.\n"
55 | " * contain one lowercase and one uppercase letter."
56 | ).format(password_length)
57 |
58 | def __repr__(self):
59 | return ""
60 |
--------------------------------------------------------------------------------
/application/flicket/views/claim.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import datetime
7 |
8 | from flask import redirect, url_for, flash, g
9 | from flask_babel import gettext
10 | from flask_login import login_required
11 |
12 | from . import flicket_bp
13 | from application import app, db
14 | from application.flicket.models.flicket_models import FlicketTicket, FlicketStatus
15 | from application.flicket.scripts.flicket_functions import add_action
16 | from application.flicket.scripts.email import FlicketMail
17 |
18 |
19 | # view for self claim a ticket
20 | @flicket_bp.route(app.config['FLICKET'] + 'ticket_claim//', methods=['GET', 'POST'])
21 | @login_required
22 | def ticket_claim(ticket_id=False):
23 | if ticket_id:
24 | # claim ticket
25 | ticket = FlicketTicket.query.filter_by(id=ticket_id).first()
26 |
27 | if ticket.assigned == g.user:
28 | flash(gettext('You have already been assigned this ticket.'), category='success')
29 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
30 |
31 | # set status to in work
32 | status = FlicketStatus.query.filter_by(status='In Work').first()
33 | ticket.assigned = g.user
34 | g.user.total_assigned += 1
35 | ticket.current_status = status
36 | ticket.last_updated = datetime.datetime.now()
37 | db.session.commit()
38 |
39 | # add action record
40 | add_action(ticket, 'claim')
41 |
42 | # send email notifications
43 | f_mail = FlicketMail()
44 | f_mail.assign_ticket(ticket=ticket)
45 |
46 | flash(gettext('You claimed ticket: %(value)s', value=ticket.id))
47 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
48 |
49 | return redirect(url_for('flicket_bp.tickets'))
50 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_deletetopic.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
48 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/forms/search.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask_babel import lazy_gettext
7 | from flask_wtf import FlaskForm
8 | from wtforms import SelectField, StringField
9 |
10 | from .flicket_forms import does_user_exist
11 | from application.flicket.models.flicket_models import FlicketDepartment
12 | from application.flicket.models.flicket_models import FlicketCategory
13 | from application.flicket.models.flicket_models import FlicketStatus
14 |
15 |
16 | class SearchTicketForm(FlaskForm):
17 |
18 | def __init__(self, *args, **kwargs):
19 | form = super(SearchTicketForm, self).__init__(*args, **kwargs)
20 |
21 | # choices are populated via ajax query on page load. This are simply empty lists so
22 | # form can be loaded on page view
23 | self.department.choices = [(d.id, d.department) for d in
24 | FlicketDepartment.query.order_by(FlicketDepartment.department.asc()).all()]
25 | self.department.choices.insert(0, (0, 'department'))
26 |
27 | self.category.choices = [(c.id, c.category) for c in
28 | FlicketCategory.query.order_by(FlicketCategory.category.asc()).all()]
29 | self.category.choices.insert(0, (0, 'category'))
30 |
31 | self.status.choices = [(s.id, s.status) for s in FlicketStatus.query.all()]
32 | self.status.choices.insert(0, (0, 'status'))
33 |
34 | """ Search form. """
35 | department = SelectField(lazy_gettext('department'), coerce=int, validators=[])
36 | category = SelectField(lazy_gettext('category'), coerce=int)
37 | status = SelectField(lazy_gettext('status'), coerce=int)
38 | username = StringField(lazy_gettext('username'), validators=[does_user_exist])
39 | content = StringField(lazy_gettext('content'), validators=[])
40 |
41 | def __repr__(self):
42 | return ""
43 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_category_edit.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
{{ _('Department') }}: {{ department }}
16 |
17 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_user_details.html:
--------------------------------------------------------------------------------
1 | {% extends 'flicket_base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
{{ title }}
10 |
11 | {{ user.name }}
12 | {% if user.avatar %}
13 |
16 | {% else %}
17 |
18 | {% endif %}
19 |
20 |
21 | {{ _('Username') }}
22 | {{ user.username }}
23 |
24 | {{ _('Email') }}
25 | {{ user.email }}
26 |
27 | {{ _('Date Joined') }}
28 | {{ user.date_added }}
29 |
30 | {{ _('Job Title') }}
31 | {{ user.job_title }}
32 |
33 | {{ _('Number Of Posts') }}
34 | {{ user.total_posts }}
35 |
36 | {{ _('Number Assigned') }}
37 | {{ user.total_assigned }}
38 |
39 |
40 |
41 |
42 |
43 |
44 | {% endblock %}
--------------------------------------------------------------------------------
/migrations/versions/bcac6741b320_hours_scale_two_dp.py:
--------------------------------------------------------------------------------
1 | """hours scale two dp
2 |
3 | Revision ID: bcac6741b320
4 | Revises: 70820003badd
5 | Create Date: 2019-11-26 12:13:16.329436
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 | # revision identifiers, used by Alembic.
12 | revision = 'bcac6741b320'
13 | down_revision = '70820003badd'
14 | branch_labels = None
15 | depends_on = None
16 |
17 |
18 | def upgrade():
19 | # ### commands auto generated by Alembic - please adjust! ###
20 | with op.batch_alter_table('flicket_post') as batch_op:
21 | batch_op.alter_column('hours',
22 | existing_type=sa.Numeric(precision=10, scale=0),
23 | type_=sa.Numeric(precision=10, scale=2),
24 | existing_nullable=True,
25 | existing_server_default=sa.text("'0'"))
26 | with op.batch_alter_table('flicket_topic') as batch_op:
27 | batch_op.alter_column('hours',
28 | existing_type=sa.Numeric(precision=10, scale=0),
29 | type_=sa.Numeric(precision=10, scale=2),
30 | existing_nullable=True,
31 | existing_server_default=sa.text("'0'"))
32 | # ### end Alembic commands ###
33 |
34 |
35 | def downgrade():
36 | # ### commands auto generated by Alembic - please adjust! ###
37 | op.alter_column('flicket_topic', 'hours',
38 | existing_type=sa.Numeric(precision=10, scale=2),
39 | type_=sa.Numeric(precision=10, scale=0),
40 | existing_nullable=True,
41 | existing_server_default=sa.text("'0'"))
42 | op.alter_column('flicket_post', 'hours',
43 | existing_type=sa.Numeric(precision=10, scale=2),
44 | type_=sa.Numeric(precision=10, scale=0),
45 | existing_nullable=True,
46 | existing_server_default=sa.text("'0'"))
47 | # ### end Alembic commands ###
48 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_history.html:
--------------------------------------------------------------------------------
1 | {% extends 'flicket_base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
{{ title }}
12 |
13 |
14 |
15 |
16 |
17 | {{ _('Post edit history.') }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 | {% if history %}
31 | {% for h in history %}
32 | {{ h.user.name }} {{ _('originally wrote on') }} {{ h.date_modified }}
33 |
34 | {% filter markdown %}
35 | {{ h.original_content }}
36 | {% endfilter %}
37 |
38 | {% endfor %}
39 | {% else %}
40 | {{ _('No changes have been made to the post content.') }}
41 | {% endif %}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket_api/views/tokens.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | """
7 |
8 | Authentication / Tokens
9 | =======================
10 |
11 | Get Token
12 | ~~~~~~~~~
13 |
14 | The user will need to provide their username and password to retrieve an authentication token. The authentication
15 | token is required to access all other parts of the API.
16 |
17 | .. code-block::
18 |
19 | # example using httpie
20 | http --auth : POST http://localhost:5000/flicket-api/tokens
21 |
22 | **Response**
23 |
24 | .. sourcecode:: http
25 |
26 | HTTP/1.0 200 OK
27 | Content-Length: 50
28 | Content-Type: application/json
29 | Date: Sat, 29 Sep 2018 14:01:00 GMT
30 | Server: Werkzeug/0.14.1 Python/3.6.5
31 |
32 | {
33 | "token": ""
34 | }
35 |
36 |
37 | Delete Token
38 | ~~~~~~~~~~~~
39 |
40 | .. code-block::
41 |
42 | # example using httpie
43 | http DELETE http://localhost:5000/flicket-api/tokens "Authorization: Bearer "
44 |
45 | **Responds**
46 |
47 | .. sourcecode:: http
48 |
49 | HTTP/1.0 204 NO CONTENT
50 | Content-Length: 0
51 | Content-Type: text/html; charset=utf-8
52 | Date: Sat, 29 Sep 2018 14:13:19 GMT
53 | Server: Werkzeug/0.14.1 Python/3.6.5
54 |
55 | """
56 |
57 | from flask import g, jsonify
58 |
59 | from .sphinx_helper import api_url
60 | from . import bp_api
61 | from application import db
62 | from application.flicket_api.views.auth import basic_auth, token_auth
63 |
64 |
65 | @bp_api.route(api_url + 'tokens', methods=['POST'])
66 | @basic_auth.login_required
67 | def get_token():
68 | token = g.current_user.get_token()
69 | db.session.commit()
70 | return jsonify({'token': token})
71 |
72 |
73 | @bp_api.route(api_url + 'tokens', methods=['DELETE'])
74 | @token_auth.login_required
75 | def revoke_token():
76 | g.current_user.revoke_token()
77 | db.session.commit()
78 | return '', 204
79 |
--------------------------------------------------------------------------------
/application/flicket/views/users.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import render_template, redirect, request, url_for
7 | from flask_babel import gettext
8 | from flask_login import login_required
9 |
10 | from application import app, flicket_bp
11 | from application.flicket.forms.flicket_forms import SearchUserForm
12 | from application.flicket.models.flicket_user import FlicketUser
13 |
14 |
15 | # view users
16 | @flicket_bp.route(app.config['FLICKET'] + 'users/', methods=['GET', 'POST'])
17 | @flicket_bp.route(app.config['FLICKET'] + 'users//', methods=['GET', 'POST'])
18 | @login_required
19 | def flicket_users(page=1):
20 | form = SearchUserForm()
21 |
22 | __filter = request.args.get('filter')
23 |
24 | if form.validate_on_submit():
25 | return redirect(url_for('flicket_bp.flicket_users', filter=form.username.data))
26 |
27 | users = FlicketUser.query
28 |
29 | if __filter:
30 | filter_1 = FlicketUser.username.ilike('%{}%'.format(__filter))
31 | filter_2 = FlicketUser.name.ilike('%{}%'.format(__filter))
32 | filter_3 = FlicketUser.email.ilike('%{}%'.format(__filter))
33 | users = users.filter(filter_1 | filter_2 | filter_3)
34 | form.username.data = __filter
35 |
36 | users = users.order_by(FlicketUser.username.asc())
37 | users = users.paginate(page=page, per_page=app.config['posts_per_page'])
38 |
39 | title = gettext('Users')
40 |
41 | return render_template('flicket_users.html',
42 | title=title,
43 | users=users,
44 | form=form)
45 |
46 |
47 | # view user details
48 | @flicket_bp.route(app.config['FLICKET'] + 'user//', methods=['GET', 'POST'])
49 | @login_required
50 | def flicket_user(user_id):
51 | user = FlicketUser.query.filter_by(id=user_id).one()
52 |
53 | title = gettext('User Details')
54 |
55 | return render_template('flicket_user_details.html',
56 | title=title,
57 | user=user)
58 |
--------------------------------------------------------------------------------
/migrations/versions/9e59e0b9d1cf_last_updated.py:
--------------------------------------------------------------------------------
1 | """last updated
2 |
3 | Revision ID: 9e59e0b9d1cf
4 | Revises: bcac6741b320
5 | Create Date: 2020-02-14 16:11:04.280946
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | from sqlalchemy.dialects import mysql
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '9e59e0b9d1cf'
14 | down_revision = 'bcac6741b320'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('flicket_topic', sa.Column('last_updated', sa.DateTime(), server_default='2016-11-21 17:58:26', nullable=True))
22 | # ### end Alembic commands ###
23 |
24 | print("Updating last_updated dates.")
25 |
26 | from application import app, db
27 | from application.flicket.models.flicket_models import FlicketTicket, FlicketPost
28 |
29 | def last_update_posts(posts):
30 |
31 | for post in posts:
32 | last_updated = post.date_added
33 |
34 | if post.date_modified:
35 | last_updated = post.date_modified
36 |
37 | return last_updated
38 |
39 | query = FlicketTicket.query.all()
40 |
41 | update_database = False
42 |
43 | for ticket in query:
44 |
45 | last_updated = ticket.date_added
46 |
47 | if ticket.date_modified:
48 | last_updated = ticket.date_modified
49 |
50 | if ticket.posts:
51 | last_update_posts(ticket.posts)
52 |
53 | if ticket.last_updated != last_updated:
54 | update_database = True
55 | ticket.last_updated = last_updated
56 |
57 | if update_database:
58 | print("Commiting changes to database.")
59 | db.session.commit()
60 | else:
61 | # prevent thread locking of database for next migration.
62 | db.session.commit()
63 | print("No database updates required.")
64 |
65 |
66 | def downgrade():
67 | # ### commands auto generated by Alembic - please adjust! ###
68 | op.drop_column('flicket_topic', 'last_updated')
69 | # ### end Alembic commands ###
70 |
71 |
--------------------------------------------------------------------------------
/application/flicket/views/create.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import (flash,
7 | redirect,
8 | url_for,
9 | request,
10 | session,
11 | render_template,
12 | g)
13 | from flask_babel import gettext
14 | from flask_login import login_required
15 |
16 | from . import flicket_bp
17 | from application import app
18 | from application.flicket.forms.flicket_forms import CreateTicketForm
19 | from application.flicket.models.flicket_models_ext import FlicketTicketExt
20 |
21 |
22 | # create ticket
23 | @flicket_bp.route(app.config['FLICKET'] + 'ticket_create/', methods=['GET', 'POST'])
24 | @login_required
25 | def ticket_create():
26 | # default category based on last submit (get from session)
27 | # using session, as information about last created ticket can be sensitive
28 | # in future it can be stored in extended user model instead
29 | last_category = session.get('ticket_create_last_category')
30 | form = CreateTicketForm(category=last_category)
31 |
32 | if form.validate_on_submit():
33 | new_ticket = FlicketTicketExt.create_ticket(title=form.title.data,
34 | user=g.user,
35 | content=form.content.data,
36 | category=form.category.data,
37 | priority=form.priority.data,
38 | hours=form.hours.data,
39 | files=request.files.getlist("file"))
40 |
41 | flash(gettext('New Ticket created.'), category='success')
42 |
43 | session['ticket_create_last_category'] = form.category.data
44 |
45 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=new_ticket.id))
46 |
47 | title = gettext('Create Ticket')
48 | return render_template('flicket_create.html', title=title, form=form)
49 |
--------------------------------------------------------------------------------
/application/flicket/views/edit_status.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import datetime
7 |
8 | from flask import redirect, url_for, g, flash
9 | from flask_babel import gettext
10 | from flask_login import login_required
11 |
12 | from . import flicket_bp
13 | from application import app, db
14 | from application.flicket.models.flicket_models import FlicketTicket, FlicketStatus
15 | from application.flicket.scripts.flicket_functions import add_action
16 | from application.flicket.scripts.email import FlicketMail
17 |
18 |
19 | # close ticket
20 | @flicket_bp.route(app.config['FLICKET'] + 'change_status///', methods=['GET', 'POST'])
21 | @login_required
22 | def change_status(ticket_id, status):
23 | ticket = FlicketTicket.query.filter_by(id=ticket_id).first()
24 | closed = FlicketStatus.query.filter_by(status=status).first()
25 |
26 | # Check to see if user is authorised to close ticket.
27 | edit = False
28 | if ticket.user == g.user:
29 | edit = True
30 | if ticket.assigned == g.user:
31 | edit = True
32 | if g.user.is_admin:
33 | edit = True
34 |
35 | if not edit:
36 | flash(gettext('Only the person to which the ticket has been assigned, creator or Admin can close this ticket.'),
37 | category='warning')
38 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket_id))
39 |
40 | # Check to see if the ticket is already closed.
41 | if ticket.current_status.status == 'Closed':
42 | flash(gettext('Ticket is already closed.'), category='warning')
43 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
44 |
45 | f_mail = FlicketMail()
46 | f_mail.close_ticket(ticket)
47 |
48 | # add action record
49 | add_action(ticket, 'close')
50 |
51 | ticket.current_status = closed
52 | ticket.assigned_id = None
53 | ticket.last_updated = datetime.datetime.now()
54 | db.session.commit()
55 |
56 | flash(gettext('Ticket %(value)s closed.', value=str(ticket_id).zfill(5)), category='success')
57 |
58 | return redirect(url_for('flicket_bp.tickets'))
59 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin_edit_group.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "flicket_base.html" %}
3 | {% block content %}
4 |
5 |
6 | {% include 'admin_menu.html' %}
7 |
8 |
9 |
{{ title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
55 |
56 |
57 |
58 |
59 |
60 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/views/release.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import datetime
7 |
8 | from flask import redirect, url_for, flash, g
9 | from flask_babel import gettext
10 | from flask_login import login_required
11 |
12 | from . import flicket_bp
13 | from application import app, db
14 | from application.flicket.models.flicket_models import FlicketTicket, FlicketStatus
15 | from application.flicket.scripts.email import FlicketMail
16 | from application.flicket.scripts.flicket_functions import add_action
17 |
18 |
19 | # view to release a ticket user has been assigned.
20 | @flicket_bp.route(app.config['FLICKET'] + 'release//', methods=['GET', 'POST'])
21 | @login_required
22 | def release(ticket_id=False):
23 |
24 | if ticket_id:
25 |
26 | ticket = FlicketTicket.query.filter_by(id=ticket_id).first()
27 |
28 | # is ticket assigned.
29 | if not ticket.assigned:
30 | flash(gettext('Ticket has not been assigned'), category='warning')
31 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket_id))
32 |
33 | # check ticket is owned by user or user is admin
34 | if (ticket.assigned.id != g.user.id) and (not g.user.is_admin):
35 | flash(gettext('You can not release a ticket you are not working on.'), category='warning')
36 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket_id))
37 |
38 | # set status to open
39 | status = FlicketStatus.query.filter_by(status='Open').first()
40 | ticket.current_status = status
41 | ticket.last_updated = datetime.datetime.now()
42 | user = ticket.assigned
43 | ticket.assigned = None
44 | user.total_assigned -= 1
45 |
46 | db.session.commit()
47 |
48 | # add action record
49 | add_action(ticket, 'release')
50 |
51 | # send email to state ticket has been released.
52 | f_mail = FlicketMail()
53 | f_mail.release_ticket(ticket)
54 |
55 | flash(gettext('You released ticket: %(value)s', value=ticket.id), category='success')
56 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
57 |
58 | return redirect(url_for('flicket_bp.tickets'))
59 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # http://www.sphinx-doc.org/en/master/config
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 |
16 | sys.path.insert(0, os.path.abspath('../application/'))
17 | # sys.path.insert(0, os.path.abspath('.'))
18 |
19 |
20 | # -- Project information -----------------------------------------------------
21 |
22 | project = 'flicket'
23 | copyright = '2024, evereux@gmail.com'
24 | author = 'evereux@gmail.com'
25 |
26 | # The full version, including alpha/beta/rc tags
27 | release = '0.3.4'
28 |
29 | # -- General configuration ---------------------------------------------------
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
33 | # ones.
34 | extensions = ['sphinx.ext.autodoc',
35 | 'sphinxcontrib.httpdomain',
36 | ]
37 |
38 | # Add any paths that contain templates here, relative to this directory.
39 | templates_path = ['_templates']
40 |
41 | # List of patterns, relative to source directory, that match files and
42 | # directories to ignore when looking for source files.
43 | # This pattern also affects html_static_path and html_extra_path.
44 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
45 |
46 | # -- Options for HTML output -------------------------------------------------
47 |
48 | # The theme to use for HTML and HTML Help pages. See the documentation for
49 | # a list of builtin themes.
50 | #
51 | html_theme = 'sphinx_rtd_theme'
52 |
53 | # Add any paths that contain custom static files (such as style sheets) here,
54 | # relative to this directory. They are copied after the builtin static files,
55 | # so a file named "default.css" will overwrite the builtin "default.css".
56 | html_static_path = ['_static']
57 |
58 | autodoc_mock_imports = ['application']
59 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_department_category.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
{{ title }}
11 |
12 |
13 |
14 |
15 |
16 |
#{{ ticket.id_zfill }} - {{ ticket.title }}
17 |
18 | {{ _('Type department and/or category and pick from drop down list to change category.') }} Available departments and
20 | categories .
21 |
22 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {% include('flicket_apijson_department_categories.html') %}
56 | {% endblock %}
57 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_assign.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 | {% include('flicket_apijson_users.html') %}
6 |
7 |
8 |
9 |
10 |
11 |
{{ title }}
12 | #{{ ticket.id_zfill }} - {{ ticket.title }}
13 |
14 |
15 |
16 |
56 |
57 | {% endblock %}
58 |
--------------------------------------------------------------------------------
/application/flicket_api/views/department_categories.py:
--------------------------------------------------------------------------------
1 | #! python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | """
7 | Department / Category
8 | =====================
9 |
10 | Get Department / Category By Category ID
11 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12 |
13 | .. http:get:: /flicket-api/department_category/(int:category_id)
14 |
15 | Get Department / Categories
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 |
18 | .. http:get:: /flicket-api/department_categories/
19 | """
20 |
21 | from flask import jsonify, request
22 |
23 | from .sphinx_helper import api_url
24 | from . import bp_api
25 | from application import app
26 | from application.flicket.models.flicket_models import FlicketDepartmentCategory
27 | from application.flicket_api.views.auth import token_auth
28 |
29 |
30 | @bp_api.route(api_url + 'department_category/', methods=['GET'])
31 | @token_auth.login_required
32 | def get_department_category(id):
33 | return jsonify(FlicketDepartmentCategory.query.get_or_404(id).to_dict())
34 |
35 |
36 | @bp_api.route(api_url + 'department_categories/', methods=['GET'])
37 | @token_auth.login_required
38 | def get_department_categories():
39 | department_category = request.args.get('department_category')
40 | department_id = request.args.get('department_id')
41 | department = request.args.get('department')
42 | department_categories = FlicketDepartmentCategory.query.order_by(FlicketDepartmentCategory.department_category)
43 | kwargs = {}
44 | if department_category:
45 | department_categories = department_categories.filter(
46 | FlicketDepartmentCategory.department_category.ilike(f'%{department_category}%'))
47 | kwargs['department_category'] = department_category
48 | if department_id:
49 | department_categories = department_categories.filter_by(department_id=department_id)
50 | kwargs['department_id'] = department_id
51 | if department:
52 | department_categories = department_categories.filter(
53 | FlicketDepartmentCategory.department.ilike(f'%{department}'))
54 | kwargs['department'] = department
55 | page = request.args.get('page', 1, type=int)
56 | per_page = min(request.args.get('per_page', app.config['posts_per_page'], type=int), 100)
57 | data = FlicketDepartmentCategory.to_collection_dict(
58 | department_categories, page, per_page, 'bp_api.get_department_categories', **kwargs)
59 | return jsonify(data)
60 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_apijson_categories.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin_delete_group.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "flicket_base.html" %}
3 | {% block content %}
4 |
5 |
6 | {% include 'admin_menu.html' %}
7 |
8 |
9 |
{{ title }}
10 |
11 |
12 |
13 |
55 |
56 |
57 | {% endblock %}
58 |
--------------------------------------------------------------------------------
/application/flicket/scripts/functions_login.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import re
7 |
8 | import bcrypt
9 | from flask_babel import gettext
10 |
11 | from application.flicket.models.flicket_user import FlicketUser
12 |
13 | password_requirements = gettext('Passwords shall adhere to the following: '
14 | '1. Be a minimum of 8 characters. '
15 | '2. Contain at least one digit. '
16 | '3. Contain at least one upper and lower case character. '
17 | '4. Not contain your username. ')
18 |
19 |
20 | def check_password_format(password, username, email):
21 | """
22 | Checks that the password adheres to the rules defined by this function.
23 | See `password_requirements`.
24 | :param password:
25 | :param username:
26 | :param email:
27 | :return: True if ok
28 | """
29 | if not ((any(s.isupper() for s in password)) and (any(s.islower() for s in password))):
30 | return False
31 | if len(password) < 8:
32 | return False
33 | if not any([c.isdigit() for c in password]):
34 | return False
35 | if username in password:
36 | return False
37 | if email in password:
38 | return False
39 |
40 | return True
41 |
42 |
43 | def check_email_format(email):
44 | """
45 | Checks that the email adheres to the rules defined by this function
46 | :param email:
47 | :return: True if ok
48 | """
49 | email_regex = re.compile(r'([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+(\.[a-zA-Z]{2,4}))')
50 | if not email_regex.match(email):
51 | return False
52 | return True
53 |
54 |
55 | def is_user_registered(username):
56 | """
57 | is the entered user registered on website?
58 | :param username:
59 | :return: True if registered
60 | """
61 |
62 | query = FlicketUser.query.filter_by(username=username)
63 | if query.count() == 1:
64 | return True
65 | return False
66 |
67 |
68 | def is_registered_password_correct(username, password):
69 | """
70 | :param username:
71 | :param password:
72 | :return: True if password is correct
73 | """
74 |
75 | user = FlicketUser.query.filter_by(username=username).first()
76 | hashed = user.password
77 | password = password.encode('utf-8')
78 |
79 | if bcrypt.hashpw(password, hashed) == hashed:
80 | return True
81 | return False
82 |
--------------------------------------------------------------------------------
/application/flicket/scripts/pie_charts.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import json
7 |
8 | import plotly
9 |
10 | from application.flicket.models.flicket_models import FlicketCategory
11 | from application.flicket.models.flicket_models import FlicketDepartment
12 | from application.flicket.models.flicket_models import FlicketStatus
13 | from application.flicket.models.flicket_models import FlicketTicket
14 |
15 |
16 | def count_department_tickets(department, status):
17 | query = FlicketTicket.query. \
18 | join(FlicketCategory). \
19 | join(FlicketStatus). \
20 | join(FlicketDepartment). \
21 | filter(FlicketDepartment.department == department). \
22 | filter(FlicketStatus.status == status)
23 |
24 | return query.count()
25 |
26 |
27 | def create_pie_chart_dict():
28 | """
29 |
30 | :return:
31 | """
32 |
33 | statii = FlicketStatus.query
34 | departments = FlicketDepartment.query
35 |
36 | graphs = []
37 |
38 | for department in departments:
39 |
40 | graph_title = department.department
41 | graph_labels = []
42 | graph_values = []
43 | for status in statii:
44 | graph_labels.append(status.status)
45 | graph_values.append(count_department_tickets(graph_title, status.status))
46 |
47 | # append graphs if have values.
48 | if any(graph_values):
49 | graphs.append(
50 | dict(
51 | data=[
52 | dict(
53 | labels=graph_labels,
54 | values=graph_values,
55 | type='pie',
56 | marker=dict(
57 | colors=['darkorange', 'darkgreen', 'green', 'lightgreen']
58 | ),
59 | sort=False
60 | )
61 | ],
62 | layout=dict(
63 | title=graph_title,
64 | autosize=True,
65 | margin=dict(
66 | b=0,
67 | t=40,
68 | l=0,
69 | r=0
70 | ),
71 | height=400,
72 |
73 | ),
74 | )
75 | )
76 |
77 | ids = [f'Graph {i}' for i, _ in enumerate(graphs)]
78 | graph_json = json.dumps(graphs, cls=plotly.utils.PlotlyJSONEncoder)
79 |
80 | return ids, graph_json
81 |
--------------------------------------------------------------------------------
/application/flicket/views/departments.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import flash, redirect, url_for, render_template
7 | from flask_babel import gettext
8 | from flask_login import login_required
9 |
10 | from . import flicket_bp
11 | from application import app, db
12 | from application.flicket.forms.flicket_forms import DepartmentForm
13 | from application.flicket.models.flicket_models import FlicketDepartment
14 |
15 |
16 | # create ticket
17 | @flicket_bp.route(app.config['FLICKET'] + 'departments/', methods=['GET', 'POST'])
18 | @flicket_bp.route(app.config['FLICKET'] + 'departments//', methods=['GET', 'POST'])
19 | @login_required
20 | def departments(page=1):
21 | form = DepartmentForm()
22 |
23 | query = FlicketDepartment.query.order_by(FlicketDepartment.department.asc())
24 |
25 | if form.validate_on_submit():
26 | add_department = FlicketDepartment(department=form.department.data)
27 | db.session.add(add_department)
28 | db.session.commit()
29 | flash(gettext('New department "{}" added.'.format(form.department.data)), category='success')
30 | return redirect(url_for('flicket_bp.departments'))
31 |
32 | _departments = query.paginate(page=page, per_page=app.config['posts_per_page'])
33 |
34 | title = gettext('Departments')
35 |
36 | return render_template('flicket_departments.html',
37 | title=title,
38 | form=form,
39 | page=page,
40 | departments=_departments)
41 |
42 |
43 | @flicket_bp.route(app.config['FLICKET'] + 'department_edit//', methods=['GET', 'POST'])
44 | @login_required
45 | def department_edit(department_id=False):
46 | if department_id:
47 |
48 | form = DepartmentForm()
49 | query = FlicketDepartment.query.filter_by(id=department_id).first()
50 |
51 | if form.validate_on_submit():
52 | query.department = form.department.data
53 | db.session.commit()
54 | flash(gettext('Department "%(value)s" edited.', value=form.department.data), category='success')
55 | return redirect(url_for('flicket_bp.departments'))
56 |
57 | form.department.data = query.department
58 |
59 | return render_template('flicket_department_edit.html',
60 | title='Edit Department',
61 | form=form,
62 | department=query
63 | )
64 |
65 | return redirect(url_for('flicket_bp.departments'))
66 |
--------------------------------------------------------------------------------
/docs/admin.rst:
--------------------------------------------------------------------------------
1 | .. _admin:
2 |
3 | Administration
4 | ==============
5 |
6 | Command Line Options
7 | --------------------
8 |
9 | From the command line the following options are available.
10 |
11 | .. module:: flicket_admin
12 |
13 | .. sourcecode::
14 |
15 | python manage.py
16 |
17 | usage: manage.py [-?]
18 | {db,run_set_up,export_users,import_users,update_user_posts,update_user_assigned,email_outstanding_tickets,runserver,shell}
19 | ...
20 |
21 | positional arguments:
22 | {db,run_set_up,export_users,import_users,update_user_posts,update_user_assigned,email_outstanding_tickets,runserver,shell}
23 | db Perform database migrations
24 | run_set_up
25 | export_users Command used by manage.py to export all the users from
26 | the database to a json file. Useful if we need a list
27 | of users to import into other applications.
28 | import_users Command used by manage.py to import users from a json
29 | file formatted such: [ { username, name, email,
30 | password. ]
31 | update_user_posts Command used by manage.py to update the users total
32 | post count. Use when upgrading from 0.1.4.
33 | update_user_assigned
34 | Command used by manage.py to update the users total
35 | post count. Use if upgrading to 0.1.7.
36 | email_outstanding_tickets
37 | Script to be run independently of the webserver.
38 | Script emails users a list of outstanding tickets that
39 | they have created or been assigned. To be run on a
40 | regular basis using a cron job or similar. Email
41 | functionality has to be enabled.
42 | runserver Runs the Flask development server i.e. app.run()
43 | shell Runs a Python shell inside Flask application context.
44 |
45 | optional arguments:
46 | -?, --help show this help message and exit
47 |
48 |
49 |
50 | Administration Config Panel
51 | ---------------------------
52 |
53 | Options
54 | ~~~~~~~
55 |
56 | For email configuration the following options are available. At a minimum you should configure `mail_server`,
57 | `mail_port`, `mail_username` and `mail_password`.
58 |
59 | For more information regarding these settings see the documentation for Flask-Mail.
60 |
61 | .. autoclass:: flicket_admin.models.flicket_config.FlicketConfig
62 | :members:
63 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_index.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{{ _('Open High Priority Tickets') }}
14 |
15 |
16 |
17 |
18 |
19 |
20 | {% if tickets.count() > 0 %}
21 |
22 |
23 | {% for t in tickets %}
24 |
25 | {% include('flicket_items.html') %}
26 |
27 | {% endfor %}
28 |
29 |
30 | {% endif %}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
{{ _('Statistics') }}
42 |
43 |
44 |
45 |
46 | {% for id in ids %}
47 |
50 | {% endfor %}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
70 |
71 | {% endblock %}
--------------------------------------------------------------------------------
/docs/install.rst:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | Installation
4 | ============
5 |
6 | First read :ref:`requirements`.
7 |
8 | It is good practise to create a virtual environment before installing
9 | the python package requirements. Virtual environments can be
10 | considered a sand boxed python installation for a specific application.
11 | They are used since one application may require a different version of
12 | a python module than another.
13 |
14 |
15 | Getting Flicket
16 | ---------------
17 |
18 | The source code for Flicket is hosted at GitHub. You can either get
19 | the latest frozen zip file or use the latest master branch.
20 |
21 |
22 | .. WARNING::
23 | If you are upgrading from a previous version please read the CHANGELOG.
24 |
25 |
26 | Master Branch
27 | ~~~~~~~~~~~~~
28 |
29 | Get the latest master branch from github using git::
30 |
31 | git clone https://github.com/evereux/flicket.git
32 |
33 | Alternatively, download and unzip the master branch `zip file `_.
34 |
35 |
36 | Installing Python Requirements
37 | ------------------------------
38 |
39 | Install the requirements using pip:::
40 |
41 | (env) C:\\flicket> pip install -r requirements.txt
42 |
43 |
44 | Set Up
45 | ------
46 |
47 | 1. If using PostgreSQL or MySQL create your database and a database user that
48 | will access the flicket database. If using SQLite you can skip this step.
49 |
50 | .. _SQLAlchemy_documentation: http://docs.sqlalchemy.org/en/latest/core/engines.html
51 |
52 | See SQLAlchemy_documentation_ for options.
53 |
54 | 2. Create the configuration json file::
55 |
56 | python -m scripts.create_json
57 |
58 | 3. If you aren't using SQLite edit `config.json` and change "db_driver".
59 | "null" should be replaced by the driver you are using. See the
60 | documentation above regarding engines, dialects and drivers. For example,
61 | if you are using a MySQL database and want to use the pymysql driver. ::
62 |
63 | "db_driver: "pymysql"
64 |
65 | 4. Install the driver you are using if not using SQLite. For example if you are
66 | using a MySQL database and want to use the pymysql driver. ::
67 |
68 | pip install pymysql
69 |
70 | 5. Upgrade the database by running the following from the command line::
71 |
72 | flask db upgrade
73 |
74 | 6. Run the set-up script:. This is required to create the Admin user and site url defaults.
75 | These can be changed again via the admin panel once you log in::
76 |
77 | flask run-set-up
78 |
79 | 7. Running development server for testing::
80 |
81 | flask run
82 |
83 |
84 | Log into the server using the username `admin` and the password defined during
85 | the setup process.
86 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ g.application_title }}{% if title %} - {{ title }} {% endif %}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 | {% include 'flicket_navbar.html' %}
41 |
42 |
43 |
44 |
45 | {{ g.application_title }}
46 |
47 | {{ _('a simple ticket system') }}
48 |
49 |
50 |
51 |
52 |
53 | {% include 'flicket_flashmessages.html' %}
54 | {% block content %}{% endblock %}
55 | {% include 'flicket_footer.html' %}
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_login.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
{{ title }}
12 |
13 |
14 |
15 |
57 |
62 |
63 |
64 |
65 |
66 |
67 | {% endblock %}
68 |
--------------------------------------------------------------------------------
/application/flicket/views/categories.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import flash, redirect, url_for, render_template
7 | from flask_login import login_required
8 | from flask_babel import gettext
9 |
10 | from . import flicket_bp
11 | from application import app, db
12 | from application.flicket.forms.flicket_forms import CategoryForm
13 | from application.flicket.models.flicket_models import FlicketCategory, FlicketDepartment
14 |
15 |
16 | # create ticket
17 | @flicket_bp.route(app.config['FLICKET'] + 'categories//', methods=['GET', 'POST'])
18 | @login_required
19 | def categories(department_id=False):
20 | form = CategoryForm()
21 | categories = FlicketCategory.query.order_by(FlicketCategory.category.asc()).filter_by(department_id=department_id)
22 | department = FlicketDepartment.query.filter_by(id=department_id).first()
23 |
24 | form.department_id.data = department_id
25 |
26 | if form.validate_on_submit():
27 | add_category = FlicketCategory(category=form.category.data, department=department)
28 | db.session.add(add_category)
29 | db.session.commit()
30 | flash(gettext('New category {} added.'.format(form.category.data)), category="success")
31 | return redirect(url_for('flicket_bp.categories', department_id=department_id))
32 |
33 | title = gettext('Categories')
34 |
35 | return render_template('flicket_categories.html',
36 | title=title,
37 | form=form,
38 | categories=categories,
39 | department=department)
40 |
41 |
42 | @flicket_bp.route(app.config['FLICKET'] + 'category_edit//', methods=['GET', 'POST'])
43 | @login_required
44 | def category_edit(category_id=False):
45 | if category_id:
46 |
47 | form = CategoryForm()
48 | category = FlicketCategory.query.filter_by(id=category_id).first()
49 | form.department_id.data = category.department_id
50 |
51 | if form.validate_on_submit():
52 | category.category = form.category.data
53 | db.session.commit()
54 | flash('Category {} edited.'.format(form.category.data), category='success')
55 | return redirect(url_for('flicket_bp.departments'))
56 |
57 | form.category.data = category.category
58 |
59 | title = gettext('Edit Category')
60 |
61 | return render_template('flicket_category_edit.html',
62 | title=title,
63 | form=form,
64 | category=category,
65 | department=category.department.department
66 | )
67 |
68 | return redirect(url_for('flicket_bp.departments'))
69 |
--------------------------------------------------------------------------------
/application/flicket/views/assign.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import datetime
7 |
8 | from flask import redirect, url_for, flash, render_template
9 | from flask_babel import gettext
10 | from flask_login import login_required
11 |
12 | from application import app, db
13 | from application.flicket.forms.flicket_forms import AssignUserForm
14 | from application.flicket.models.flicket_models import FlicketTicket, FlicketStatus, FlicketSubscription
15 | from application.flicket.models.flicket_user import FlicketUser
16 | from application.flicket.scripts.flicket_functions import add_action
17 | from application.flicket.scripts.email import FlicketMail
18 | from . import flicket_bp
19 |
20 |
21 | # tickets main
22 | @flicket_bp.route(app.config['FLICKET'] + 'ticket_assign//', methods=['GET', 'POST'])
23 | @login_required
24 | def ticket_assign(ticket_id=False):
25 | form = AssignUserForm()
26 | ticket = FlicketTicket.query.filter_by(id=ticket_id).one()
27 |
28 | if ticket.current_status.status == 'Closed':
29 | flash(gettext("Can't assign a closed ticket."), category='warning')
30 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket_id))
31 |
32 | if form.validate_on_submit():
33 |
34 | user = FlicketUser.query.filter_by(username=form.username.data).first()
35 |
36 | if ticket.assigned == user:
37 | flash(gettext('User is already assigned to ticket.'), category='warning')
38 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
39 |
40 | # set status to in work
41 | status = FlicketStatus.query.filter_by(status='In Work').first()
42 | # assign ticket
43 | ticket.assigned = user
44 | ticket.current_status = status
45 | ticket.last_updated = datetime.datetime.now()
46 |
47 | if not user.total_assigned:
48 | user.total_assigned = 1
49 | else:
50 | user.total_assigned += 1
51 |
52 | # add action record
53 | add_action(ticket, 'assign', recipient=user)
54 |
55 | # subscribe to the ticket
56 | if not ticket.is_subscribed(user):
57 | subscribe = FlicketSubscription(
58 | ticket=ticket,
59 | user=user
60 | )
61 | db.session.add(subscribe)
62 |
63 | db.session.commit()
64 |
65 | # send email to state ticket has been assigned.
66 | f_mail = FlicketMail()
67 | f_mail.assign_ticket(ticket)
68 |
69 | flash(gettext('You reassigned ticket: {} to {}'.format(ticket.id, user.name)), category='success')
70 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
71 |
72 | title = gettext('Assign Ticket')
73 |
74 | return render_template("flicket_assign.html", title=title, form=form, ticket=ticket)
75 |
--------------------------------------------------------------------------------
/application/flicket/views/department_category.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import abort, redirect, url_for, flash, render_template, g
7 | from flask_babel import gettext
8 | from flask_login import login_required
9 |
10 | from application import app, db
11 | from application.flicket.forms.flicket_forms import ChangeDepartmentCategoryForm
12 | from application.flicket.models.flicket_models import FlicketTicket
13 | from application.flicket.models.flicket_models import FlicketDepartmentCategory
14 | from application.flicket.scripts.flicket_functions import add_action
15 | from . import flicket_bp
16 |
17 |
18 | # tickets main
19 | @flicket_bp.route(app.config['FLICKET'] + 'ticket_department_category//', methods=['GET', 'POST'])
20 | @login_required
21 | def ticket_department_category(ticket_id=False):
22 | if not app.config['change_category']:
23 | abort(404)
24 |
25 | if app.config['change_category_only_admin_or_super_user']:
26 | if not g.user.is_admin and not g.user.is_super_user:
27 | abort(404)
28 |
29 | form = ChangeDepartmentCategoryForm()
30 | ticket = FlicketTicket.query.get_or_404(ticket_id)
31 |
32 | if ticket.current_status.status == 'Closed':
33 | flash(gettext("Can't change the department and category on a closed ticket."))
34 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket_id))
35 |
36 | if form.validate_on_submit():
37 | department_category = FlicketDepartmentCategory.query.filter_by(
38 | department_category=form.department_category.data).one()
39 |
40 | if ticket.category_id == department_category.category_id:
41 | flash(gettext(
42 | 'Category "{} / {}" '
43 | 'is already assigned to ticket.'.format(ticket.category.category, ticket.category.department.department)),
44 | category='warning')
45 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
46 |
47 | # change category
48 | ticket.category_id = department_category.category_id
49 |
50 | # add action record
51 | add_action(ticket, 'department_category', data={
52 | 'department_category': department_category.department_category,
53 | 'category_id': department_category.category_id,
54 | 'category': department_category.category,
55 | 'department_id': department_category.department_id,
56 | 'department': department_category.department})
57 |
58 | db.session.commit()
59 |
60 | flash(gettext('You changed category of ticket: {}'.format(ticket_id)), category='success')
61 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket.id))
62 |
63 | title = gettext('Change Department / Category of Ticket')
64 |
65 | return render_template("flicket_department_category.html", title=title, form=form, ticket=ticket)
66 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin_groups.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "flicket_base.html" %}
3 | {% block content %}
4 |
5 |
6 |
7 | {% include 'admin_menu.html' %}
8 |
9 |
10 |
{{ title }}
11 |
12 | {{ _('Click on group name to edit.') }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
{{ _('Group Name') }}
21 |
{{ _('Delete') }}
22 |
23 | {%- for group in groups -%}
24 |
36 | {%- endfor -%}
37 |
38 |
39 |
{{ _('Add Group') }}
40 |
66 |
67 |
68 |
69 | {% endblock %}
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_categories.html:
--------------------------------------------------------------------------------
1 | {% extends "flicket_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
{{ title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Department: {{ department.department }}
19 |
20 |
21 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
{{ _('Existing Categories') }}
53 |
54 | {% for c in categories %}
55 |
68 | {% endfor %}
69 |
70 |
71 |
72 |
73 | {% endblock %}
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from alembic import context
3 | from sqlalchemy import engine_from_config, pool
4 | from logging.config import fileConfig
5 | import logging
6 |
7 | # this is the Alembic Config object, which provides
8 | # access to the values within the .ini file in use.
9 | config = context.config
10 |
11 | # Interpret the config file for Python logging.
12 | # This line sets up loggers basically.
13 | fileConfig(config.config_file_name)
14 | logger = logging.getLogger('alembic.env')
15 |
16 | # add your model's MetaData object here
17 | # for 'autogenerate' support
18 | # from myapp import mymodel
19 | # target_metadata = mymodel.Base.metadata
20 | from flask import current_app
21 | config.set_main_option('sqlalchemy.url',
22 | current_app.config.get('SQLALCHEMY_DATABASE_URI'))
23 | target_metadata = current_app.extensions['migrate'].db.metadata
24 |
25 | # other values from the config, defined by the needs of env.py,
26 | # can be acquired:
27 | # my_important_option = config.get_main_option("my_important_option")
28 | # ... etc.
29 |
30 |
31 | def run_migrations_offline():
32 | """Run migrations in 'offline' mode.
33 |
34 | This configures the context with just a URL
35 | and not an Engine, though an Engine is acceptable
36 | here as well. By skipping the Engine creation
37 | we don't even need a DBAPI to be available.
38 |
39 | Calls to context.execute() here emit the given string to the
40 | script output.
41 |
42 | """
43 | url = config.get_main_option("sqlalchemy.url")
44 | context.configure(url=url)
45 |
46 | with context.begin_transaction():
47 | context.run_migrations()
48 |
49 |
50 | def run_migrations_online():
51 | """Run migrations in 'online' mode.
52 |
53 | In this scenario we need to create an Engine
54 | and associate a connection with the context.
55 |
56 | """
57 |
58 | # this callback is used to prevent an auto-migration from being generated
59 | # when there are no changes to the schema
60 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
61 | def process_revision_directives(context, revision, directives):
62 | if getattr(config.cmd_opts, 'autogenerate', False):
63 | script = directives[0]
64 | if script.upgrade_ops.is_empty():
65 | directives[:] = []
66 | logger.info('No changes in schema detected.')
67 |
68 | engine = engine_from_config(config.get_section(config.config_ini_section),
69 | prefix='sqlalchemy.',
70 | poolclass=pool.NullPool)
71 |
72 | connection = engine.connect()
73 | context.configure(connection=connection,
74 | target_metadata=target_metadata,
75 | process_revision_directives=process_revision_directives,
76 | **current_app.extensions['migrate'].configure_args)
77 |
78 | try:
79 | with context.begin_transaction():
80 | context.run_migrations()
81 | finally:
82 | connection.close()
83 |
84 | if context.is_offline_mode():
85 | run_migrations_offline()
86 | else:
87 | run_migrations_online()
88 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_items.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 | {{ t.title }}
10 |
11 |
12 | {{ _('Replies') }}: {{ t.num_replies }}
13 |
14 |
15 | Hours: {{ t.total_hours }}
16 |
17 |
18 |
19 |
21 |
22 |
23 | {{ _('priority') }}
24 |
25 |
34 | {{ t.ticket_priority.priority }}
35 |
36 |
37 |
38 |
39 | {{ _('submitted by') }}
40 |
41 |
42 | {{ t.user.name }}
43 |
44 |
45 |
46 |
47 | {{ _('date') }}
48 |
49 |
50 | {{ t.date_added.strftime('%Y-%m-%d') }}
51 |
52 |
53 |
54 |
55 | {{ _('department / category') }}
56 |
57 |
58 | {{ t.category.department.department }} / {{ t.category.category }}
59 |
60 |
61 |
62 |
63 | {{ _('status') }}
64 |
65 |
66 | {{ t.current_status.status }}
67 |
68 |
69 |
70 |
71 | {{ _('assigned') }}
72 |
73 |
74 | {{ t.assigned.name }}
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/application/flicket_admin/forms/form_config.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask_babel import lazy_gettext
7 | from flask_wtf import FlaskForm
8 | from wtforms import StringField, SubmitField, IntegerField, BooleanField, PasswordField
9 | from wtforms.validators import DataRequired, NumberRange, Length
10 |
11 | from application.flicket.scripts.functions_login import check_email_format
12 |
13 | form_class_button = {'class': 'btn btn-primary btn-sm'}
14 |
15 |
16 | def check_email_formatting(form, field):
17 | """
18 | Checks formatting of email and also checks that a user is not already registered with the same email address
19 | :param form:
20 | :param field:
21 | :return:
22 | """
23 | ok = True
24 | if not check_email_format(form.email_address.data):
25 | field.errors.append('Please enter a correctly formatted email address.')
26 | ok = False
27 |
28 | return ok
29 |
30 |
31 | class ConfigForm(FlaskForm):
32 | mail_server = StringField(lazy_gettext('mail_server'), validators=[])
33 | mail_port = IntegerField(lazy_gettext('mail_port'), validators=[NumberRange(min=1, max=65535)])
34 | mail_use_tls = BooleanField(lazy_gettext('mail_use_tls'), validators=[])
35 | mail_use_ssl = BooleanField(lazy_gettext('mail_use_ssl'), validators=[])
36 | mail_debug = BooleanField(lazy_gettext('mail_debug'), validators=[])
37 | mail_username = StringField(lazy_gettext('mail_username'), validators=[])
38 | mail_password = PasswordField(lazy_gettext('mail_password'), validators=[])
39 | mail_default_sender = StringField(lazy_gettext('mail_default_sender'), validators=[])
40 | mail_max_emails = IntegerField(lazy_gettext('mail_max_emails'), validators=[])
41 | mail_suppress_send = BooleanField(lazy_gettext('mail_suppress_send'), validators=[])
42 | mail_ascii_attachments = BooleanField(lazy_gettext('mail_ascii_attachments'), validators=[])
43 |
44 | application_title = StringField(lazy_gettext('application_title'),
45 | validators=[DataRequired(), Length(min=3, max=32)])
46 | posts_per_page = IntegerField(lazy_gettext('posts_per_page'),
47 | validators=[DataRequired(), NumberRange(min=10, max=200)])
48 | allowed_extensions = StringField(lazy_gettext('allowed_extensions'), validators=[DataRequired()])
49 | ticket_upload_folder = StringField(lazy_gettext('ticket_upload_folder'), validators=[DataRequired()])
50 | base_url = StringField(lazy_gettext('base_url'), validators=[Length(min=0, max=128)])
51 |
52 | use_auth_domain = BooleanField(lazy_gettext('use_auth_domain'), validators=[])
53 | auth_domain = StringField(lazy_gettext('auth_domain'), validators=[])
54 |
55 | csv_dump_limit = IntegerField(lazy_gettext('csv_dump_limit'), validators=[])
56 |
57 | change_category = BooleanField(lazy_gettext('change_category'), validators=[])
58 | change_category_only_admin_or_super_user = BooleanField(lazy_gettext('change_category_only_admin_or_super_user'),
59 | validators=[])
60 |
61 | submit = SubmitField(lazy_gettext('Submit'), render_kw=form_class_button, validators=[DataRequired()])
62 |
63 |
64 | class EmailTest(FlaskForm):
65 | email_address = StringField(lazy_gettext('email_address'), validators=[DataRequired(), check_email_formatting])
66 |
67 | submit = SubmitField(lazy_gettext('Submit'), render_kw=form_class_button, validators=[DataRequired()])
68 |
--------------------------------------------------------------------------------
/application/flicket/views/user_edit.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | from flask import flash, g, redirect, render_template, request, url_for
7 | from flask_login import login_required
8 |
9 | from application import app, db
10 | from application.flicket.forms.forms_main import EditUserForm
11 | from application.flicket.models.flicket_user import FlicketUser
12 | from application.flicket.scripts.flicket_upload import UploadAvatar
13 | from application.flicket.scripts.functions_login import check_password_format, password_requirements
14 | from application.flicket.scripts.hash_password import hash_password
15 | from . import flicket_bp
16 |
17 |
18 | # edit self page
19 | @flicket_bp.route(app.config['WEBHOME'] + 'user_details', methods=['GET', 'POST'])
20 | @login_required
21 | def user_details():
22 | form = EditUserForm()
23 |
24 | if form.validate_on_submit():
25 |
26 | if 'avatar' in request.files:
27 | avatar = request.files['avatar']
28 | filename = avatar.filename
29 | else:
30 | avatar = False
31 | filename = ''
32 |
33 | if filename != '':
34 | # upload the avatar
35 | upload_avatar = UploadAvatar(avatar, g.user)
36 | if upload_avatar.upload_file() is False:
37 | flash('There was a problem uploading files. Please ensure you are using a valid image file name.',
38 | category='danger')
39 | return redirect(url_for('flicket_bp.user_details'))
40 | avatar_filename = upload_avatar.file_name
41 | else:
42 | avatar_filename = None
43 |
44 | # find the user in db to edit
45 | user = FlicketUser.query.filter_by(id=g.user.id).first()
46 |
47 | # update details, if changed
48 | if user.name != form.name.data:
49 | user.name = form.name.data
50 | flash('You have changed your "name".', category='success')
51 | if user.email != form.email.data:
52 | user.email = form.email.data
53 | flash('You have changed your "email".', category='success')
54 | if user.job_title != form.job_title.data:
55 | user.job_title = form.job_title.data
56 | flash('You have changed your "job title".', category='success')
57 | if user.locale != form.locale.data:
58 | user.locale = form.locale.data
59 | flash('You have changed your "locale".', category='success')
60 |
61 | if avatar_filename:
62 | user.avatar = avatar_filename
63 |
64 | # change the password if the user has entered a new password.
65 | password = form.new_password.data
66 | if (password != '') and (check_password_format(password, user.username, user.email)):
67 | password = hash_password(password)
68 | user.password = password
69 | flash('You have changed your password.', category='success')
70 | elif password != '':
71 | flash('Password not changed.', category='warning')
72 | flash(password_requirements, category='warning')
73 |
74 | db.session.commit()
75 |
76 | return redirect(url_for('flicket_bp.user_details'))
77 |
78 | form.name.data = g.user.name
79 | form.email.data = g.user.email
80 | form.username.data = g.user.username
81 | form.job_title.data = g.user.job_title
82 | form.locale.data = g.user.locale
83 |
84 | return render_template('flicket_edituser.html', form=form, title='Edit User Details')
85 |
--------------------------------------------------------------------------------
/application/flicket/views/subscribe.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 |
4 | # ! usr/bin/python3
5 | # -*- coding: utf-8 -*-
6 | #
7 | # Flicket - copyright Paul Bourne: evereux@gmail.com
8 |
9 | import datetime
10 |
11 | from flask import flash, g, redirect, url_for
12 | from flask_babel import gettext
13 | from flask_login import login_required
14 | from flask import abort
15 | from flask import render_template
16 |
17 | from application import app, db
18 | from application.flicket.forms.flicket_forms import UnSubscribeUser
19 | from application.flicket.models.flicket_models import FlicketSubscription
20 | from application.flicket.models.flicket_models import FlicketTicket
21 | from application.flicket.models.flicket_user import FlicketUser
22 | from application.flicket.scripts.flicket_functions import add_action
23 | from . import flicket_bp
24 |
25 |
26 | # # view to unsubscribe user from a ticket.
27 | # @flicket_bp.route(app.config['FLICKET'] + 'unsubscribe//', methods=['GET', 'POST'])
28 | # @login_required
29 | # def unsubscribe_ticket(ticket_id=None, user_id=None):
30 | # if ticket_id and user_id:
31 | #
32 | # ticket = FlicketTicket.query.filter_by(id=ticket_id).one()
33 | # user = FlicketUser.query.filter_by(id=user_id).one()
34 | #
35 | # if ticket.can_unsubscribe(user):
36 | # subscription = FlicketSubscription.query.filter_by(user=user, ticket=ticket).one()
37 | # # unsubscribe user to ticket
38 | # ticket.last_updated = datetime.datetime.now()
39 | # add_action(ticket, 'unsubscribe', recipient=user)
40 | # db.session.delete(subscription)
41 | # db.session.commit()
42 | # flash(gettext('"{}" has been unsubscribed from this ticket.'.format(user.name)), category='success')
43 | #
44 | # else:
45 | #
46 | # flash(gettext('Could not unsubscribe "{}" from ticket.'.format(user.name)), category='warning')
47 | #
48 | # return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket_id))
49 |
50 | # view to unsubscribe user from a ticket.
51 | @flicket_bp.route(app.config['FLICKET'] + 'unsubscribe//', methods=['GET', 'POST'])
52 | @login_required
53 | def unsubscribe_ticket(ticket_id=None, user_id=None):
54 | if not ticket_id and user_id:
55 | return abort(404)
56 |
57 | form = UnSubscribeUser()
58 |
59 | ticket = FlicketTicket.query.filter_by(id=ticket_id).one()
60 | user = FlicketUser.query.filter_by(id=user_id).one()
61 |
62 | form.username.data = user.username
63 |
64 | if form.validate_on_submit():
65 |
66 | if ticket.can_unsubscribe(user):
67 | subscription = FlicketSubscription.query.filter_by(user=user, ticket=ticket).one()
68 | # unsubscribe user to ticket
69 | ticket.last_updated = datetime.datetime.now()
70 | add_action(ticket, 'unsubscribe', recipient=user)
71 | db.session.delete(subscription)
72 | db.session.commit()
73 | flash(gettext('"{}" has been unsubscribed from this ticket.'.format(user.name)), category='success')
74 |
75 | else:
76 |
77 | flash(gettext('Could not unsubscribe "{}" from ticket due to permission restrictions.'.format(user.name)),
78 | category='warning')
79 |
80 | return redirect(url_for('flicket_bp.ticket_view', ticket_id=ticket_id))
81 |
82 | # else:
83 | # print(form.errors)
84 |
85 | return render_template('flicket_unsubscribe_user.html', form=form, title='Unsubscribe', ticket=ticket, user=user)
86 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin_delete_user.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "flicket_base.html" %}
3 | {% block content %}
4 |
5 |
6 | {% include 'admin_menu.html' %}
7 |
8 |
9 |
{{ title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
72 |
73 |
74 |
75 |
76 | {% endblock %}
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf8 -*-
3 |
4 |
5 | import json
6 | import os
7 | import platform
8 |
9 | from scripts.create_json import config_file
10 | from scripts.create_json import WriteConfigJson
11 | from scripts.create_json import check_db_connection
12 |
13 | basedir = os.path.abspath(os.path.dirname(__file__))
14 |
15 |
16 | class BaseConfiguration(object):
17 |
18 | WriteConfigJson.json_exists()
19 |
20 | DEBUG = False
21 | TESTING = False
22 | EXPLAIN_TEMPLATE_LOADING = False
23 |
24 | try:
25 |
26 | # get data from config file
27 | with open(config_file, 'r') as f:
28 | config_data = json.load(f)
29 |
30 | # user login information for database user.
31 | db_username = config_data['db_username']
32 | db_password = config_data['db_password']
33 | # database connection details
34 | db_url = config_data['db_url']
35 | db_port = config_data['db_port']
36 | db_name = config_data['db_name']
37 | db_type = config_data['db_type']
38 | db_driver = config_data['db_driver']
39 |
40 | except KeyError:
41 | raise KeyError('The file config.json appears to incorrectly formatted.')
42 |
43 | db_dialect = None
44 | SQLALCHEMY_DATABASE_URI = None
45 |
46 | sql_os_path_prefix = '////'
47 | if platform.system() == 'Windows':
48 | sql_os_path_prefix = '///'
49 |
50 | if db_type == 1:
51 | db_dialect = 'sqlite'
52 | db_path = os.path.join(basedir, db_name)
53 | SQLALCHEMY_DATABASE_URI = f'{db_dialect}:{sql_os_path_prefix}{db_path}'
54 |
55 | else:
56 |
57 | if db_type == 2:
58 | db_dialect = 'postgresql'
59 | if db_type == 3:
60 | db_dialect = 'mysql'
61 |
62 | SQLALCHEMY_DATABASE_URI = f'{db_dialect}+{db_driver}://{db_username}:{db_password}@{db_url}:{db_port}/{db_name}'
63 |
64 | if SQLALCHEMY_DATABASE_URI is None:
65 | raise ConnectionAbortedError('Incorrect database type defined in config.json.')
66 |
67 | SQLALCHEMY_TRACK_MODIFICATIONS = True
68 |
69 | # default flicket_admin group name
70 | ADMIN_GROUP_NAME = 'flicket_admin'
71 | SUPER_USER_GROUP_NAME = 'super_user'
72 |
73 | SECRET_KEY = config_data['SECRET_KEY']
74 |
75 | # The base url for your application.
76 | WEBHOME = '/'
77 | # The base url for flicket.
78 | FLICKET = WEBHOME + ''
79 | FLICKET_API = WEBHOME + 'flicket-api/'
80 | FLICKET_REST_API = WEBHOME + 'flicket-rest-api'
81 | ADMINHOME = '/flicket_admin/'
82 |
83 | # flicket user used to post replies to tickets for status changes.
84 | NOTIFICATION = {'name': 'notification',
85 | 'username': 'notification',
86 | 'password': config_data['NOTIFICATION_USER_PASSWORD'],
87 | 'email': 'admin@localhost'}
88 |
89 | SUPPORTED_LANGUAGES = {'en': 'English', 'fr': 'Francais'}
90 | BABEL_DEFAULT_LOCALE = 'en'
91 | BABEL_DEFAULT_TIMEZONE = 'UTC'
92 |
93 | check_db_connection(SQLALCHEMY_DATABASE_URI)
94 |
95 |
96 | class TestConfiguration(BaseConfiguration):
97 | DEBUG = False
98 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'test.db')
99 | WTF_CSRF_ENABLED = False
100 | TESTING = True
101 | SESSION_PROTECTION = None
102 | LOGIN_DISABLED = False
103 | SERVER_NAME = 'localhost:5001'
104 | config_data = {"db_username": "", "db_port": "", "db_password": "",
105 | "db_name": "", "db_url": ""}
106 |
--------------------------------------------------------------------------------
/application/flicket/forms/form_login.py:
--------------------------------------------------------------------------------
1 | #! usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Flicket - copyright Paul Bourne: evereux@gmail.com
5 |
6 | import bcrypt
7 | from flask_wtf import FlaskForm
8 | from flask_babel import lazy_gettext
9 | from sqlalchemy import func, or_
10 | from wtforms import BooleanField
11 | from wtforms import PasswordField
12 | from wtforms import StringField
13 | from wtforms import SubmitField
14 | from wtforms.validators import DataRequired
15 |
16 | from application import app
17 | from application.flicket.models.flicket_user import FlicketUser
18 | from application.flicket.scripts.hash_password import hash_password
19 | from application.flicket_admin.views.view_admin import create_user
20 | from application.flicket.forms.flicket_forms import form_class_button
21 | from scripts.login_functions import nt_log_on
22 |
23 |
24 | def login_user_exist(form, field):
25 | """
26 | Ensure the username exists.
27 | :param form:
28 | :param field:
29 | :return True False:
30 | """
31 |
32 | username = form.username.data
33 | password = form.password.data
34 |
35 | if app.config['use_auth_domain']:
36 | nt_authenticated = nt_log_on(app.config['auth_domain'], username, password)
37 | else:
38 | nt_authenticated = False
39 |
40 | result = FlicketUser.query.filter(
41 | or_(func.lower(FlicketUser.username) == username.lower(), func.lower(FlicketUser.email) == username.lower()))
42 | if result.count() == 0:
43 | # couldn't find username in database so check if the user is authenticated on the domain.
44 | if nt_authenticated:
45 | # user might have tried to login with full email?
46 | username = username.split('@')[0]
47 | # create the previously unregistered user.
48 | create_user(username, password, name=username)
49 | else:
50 | # user can't be authenticated on the domain or found in the database.
51 | field.errors.append('Invalid username or email.')
52 | return False
53 | result = result.first()
54 | if bcrypt.hashpw(password.encode('utf-8'), result.password) != result.password:
55 | if nt_authenticated:
56 | # update password in database.
57 | result.password = hash_password(password)
58 | return True
59 | field.errors.append('Invalid password. Please contact admin is this problem persists.')
60 | return False
61 |
62 | return True
63 |
64 |
65 | def is_disabled(form, field):
66 | """
67 | Ensure the username exists.
68 | :param form:
69 | :param field:
70 | :return True False:
71 | """
72 | username = form.username.data
73 |
74 | user = FlicketUser.query.filter(
75 | or_(func.lower(FlicketUser.username) == username.lower(), func.lower(FlicketUser.email) == username.lower()))
76 | if user.count() == 0:
77 | return False
78 | user = user.first()
79 | if user.disabled:
80 | field.errors.append('Account has been disabled.')
81 | return False
82 |
83 | return True
84 |
85 |
86 | class LogInForm(FlaskForm):
87 | """ Log in form. """
88 | username = StringField(lazy_gettext('username'), validators=[DataRequired(), login_user_exist, is_disabled])
89 | password = PasswordField(lazy_gettext('password'), validators=[DataRequired()])
90 | remember_me = BooleanField(lazy_gettext('remember_me'), default=False)
91 |
92 |
93 | class PasswordResetForm(FlaskForm):
94 | """ Log in form. """
95 | email = StringField(lazy_gettext('email'), validators=[DataRequired()])
96 | submit = SubmitField(lazy_gettext('reset password'), render_kw=form_class_button)
97 |
--------------------------------------------------------------------------------
/application/flicket/templates/flicket_form_reply.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ pagedown.html_head() }}
4 |
5 |
87 |
88 |
--------------------------------------------------------------------------------
/application/flicket_admin/templates/admin_config.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "flicket_base.html" %}
3 | {% block content %}
4 |
5 |
6 |
7 | {% include 'admin_menu.html' %}
8 |
9 |
10 |
11 |
12 |
13 |
{{ _('Configuration') }}
14 |
15 |
16 |
17 |
76 |
77 |
78 |
79 |
80 | {% endblock %}
--------------------------------------------------------------------------------
/docs/faq.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Flicket - FAQ
3 | =============
4 |
5 | What is Flicket?
6 | ----------------
7 |
8 | Flicket is a simple open source ticketing system driven by the python
9 | flask web micro framework.
10 |
11 | Flicket also uses the following python packages:
12 |
13 | alembic, bcrypt, flask-admin, flask-babel, flask-login, flask-migrate,
14 | flask-principal, flask-sqlalchemy, flask-script, flask-wtf, jinja2,
15 | Markdown, WTForms
16 |
17 | See `README.rst` for full requirements.
18 |
19 | ## Licensing
20 |
21 | For licensing see `LICENSE.md`
22 |
23 | Tickets
24 | -------
25 |
26 | General
27 | ~~~~~~~~~~~
28 | 1. How do I create a ticket?
29 |
30 | Select 'create ticket' from the Flicket pull down menu.
31 |
32 | 2. How do I assign a ticket?
33 |
34 | Scenario: You have raised a ticket and you know to whom the ticket
35 | should be assigned.
36 |
37 | Navigate to [flicket home page](/flicket/) and select the ticket you
38 | wish to assign. Within the ticket page is a button to `assign` ticket.
39 |
40 | 3. How do I release a ticket?
41 |
42 | Scenario: You have been assigned a ticket but the ticket isn't your
43 | responsibility to complete or you are unable to for another reason.
44 |
45 | Navigate to [flicket home page](/flicket/) and select the ticket to
46 | which you have been assigned. Within the ticket page is a button
47 | to `release` the ticket from your ticket list.
48 |
49 | 4. How do I close a ticket?
50 |
51 | Scenario: The ticket has been resolved to your satisfaction and you
52 | want to close the ticket.
53 |
54 | Navigate to [flicket home page](/flicket/) and select the ticket
55 | which you would like to close. Within the ticket page is a button
56 | to `replay and close` the ticket.
57 |
58 | Only the following persons can close a ticket:
59 | * Administrators.
60 | * The user which has been assigned the ticket.
61 | * The original creator of the ticket.
62 |
63 | You may `claim` the ticket so that you may close it.
64 |
65 | 5. What is markdown?
66 |
67 | Markdown is a lightweight markup language with plain text formatting syntax.
68 |
69 | The text contents of a ticket can be made easier to read by employing
70 | markdown syntax.
71 |
72 | 6. How do I change the locale (language settings)?
73 |
74 | In the top right hand corner click on your profile and select `User Details`.
75 | Within the `Edit User Details` page you can pick your locale. Locales can
76 | also be set on user creation.
77 |
78 | If you'd like to add a new locale see the section `Adding Additional Languages`.
79 |
80 |
81 | Searching
82 | ~~~~~~~~~
83 |
84 | The ticket main page can be filtered to show only results of a specific
85 | interest to you. Tickets can be filtered by department, category, user
86 | and a text string.
87 |
88 |
89 | Departments
90 | ~~~~~~~~~~~
91 |
92 | .. note::
93 | Only administrators or super users can add / edit or delete departments.
94 |
95 | 1. How do I add new departments?
96 |
97 | Navigate to Departments via the menu bar and use the add departments form.
98 |
99 | 2. How do I edit departments?
100 |
101 | Navigate to [departments](/flicket/departments/) and select the edit
102 | link against the department name.
103 |
104 | 3. How do I delete departments?
105 |
106 | Navigate to [departments](/flicket/departments/) and select the remove
107 | link against the department name. This is represented with a cross.
108 |
109 |
110 | Categories
111 | ~~~~~~~~~~
112 |
113 | .. note::
114 | Only administrators or super users can add / edit or delete categories.
115 |
116 | 1. How do I add categories?
117 |
118 | Navigate to [departments](/flicket/departments/) and select the link
119 | to add categories against the appropriate department name.
120 |
121 | 1. How do I edit categories?
122 |
123 | Navigate to [departments](/flicket/departments/) and select the link
124 | to add categories against the appropriate department name.
--------------------------------------------------------------------------------