├── 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 |
5 |

{{ _('404 Error - File Not Found') }}

6 |

{{ _('Back') }}

7 |
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 | 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 |
6 |

{{ title }}

7 |
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 | 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 | 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 | 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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
{{ _('Status') }}{{ ticket.current_status.status }}
{{ _('Priority') }}{{ ticket.ticket_priority.priority }}
{{ _('Assigned') }}{% if ticket.assigned.username %}{{ ticket.assigned.name }}{% else %}ticket not claimed{% endif %}
{{ _('Content') }}{{ ticket.content }}
-------------------------------------------------------------------------------- /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 | 13 | 14 | 15 | 16 | 17 | 18 | {% for ticket in tickets %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 |
Ticket NumberTitlePriorityStatusAssigned
{{ ticket.id_zfill }}{{ ticket.title }}{{ ticket.ticket_priority.priority }}{{ ticket.current_status.status }}{{ ticket.assigned.name }}
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 | -------------------------------------------------------------------------------- /application/flicket/templates/flicket_password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "flicket_base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |

{{ title }}

7 |
8 |
9 |
10 |   11 |
12 |
13 | 14 | {{ form.hidden_tag() }} 15 | 16 |
17 | {{ form.email(class="form-control") }} 18 |
19 |
20 |
21 |   22 |
23 |
24 |
25 |
26 | {{ form.submit }} 27 |
28 | 29 |
30 |
31 |
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 | 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 |
13 | 14 |
19 | {{ form.hidden_tag() }} 20 | 23 |
24 | {{ form.department(class="form-control form-control-sm") }} 25 |
26 |
27 | 28 |
29 | {% if form.department.errors %} 30 |
31 | {% for error in form.department.errors %} 32 | {{ error }} 33 | {% endfor %} 34 |
35 | {% endif %} 36 |
37 |
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 |
18 | {{ form.hidden_tag() }} 19 |
20 | 21 |
22 | {{ form.password(class="form-control") }} 23 |
24 |
25 | 26 |
27 |
28 | {% if form.password.errors %} 29 |
30 |
31 | {% for error in form.password.errors %} 32 | {{ error }} 33 | {% endfor %} 34 |
35 |
36 | {% endif %} 37 |
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 |
14 |
15 |
16 | {{ form.hidden_tag() }} 17 | 18 |
19 |
20 | {{ form.email_address.label }} 21 |
22 |
{{ form.email_address(class="form-control form-control-sm") }}
23 |
24 | 25 |
26 |
27 | {{ form.submit }} 28 |
29 |
30 | 31 |
32 |
33 |
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 |
13 |
14 | {{ form.hidden_tag() }} 15 |
16 |
17 |
18 |

{{ notification }}

19 |
20 |
21 |
22 | 23 |
24 | {{ form.password(class="form-control form-control-sm") }} 25 |
26 |
27 | 28 |
29 | {% if form.password.errors %} 30 |
31 | {% for error in form.password.errors %} 32 | {{ error }} 33 | {% endfor %} 34 |
35 | {% endif %} 36 |
37 |
38 |
39 |
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 |
5 | 6 |
7 |
8 | {{ form.hidden_tag() }} 9 | 10 |
11 | 12 |
13 |
14 |

{{ title }}

15 |
16 |
17 |
18 |
19 |

{{ ticket.id_zfill }} - {{ ticket.title }}

20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 | {{ form.password(class="form-control form-control-sm") }} 28 |
29 |
30 | 31 |
32 |
33 | 34 | {% if form.password.errors %} 35 |
36 |
37 | {% for error in form.password.errors %} 38 | {{ error }} 39 | {% endfor %} 40 |
41 |
42 | {% endif %} 43 | 44 |
45 |
46 | 47 |
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 |
7 |

{{ title }}

8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 |
{{ _('Department') }}: {{ department }}
16 |
17 |
22 | {{ form.hidden_tag() }} 23 |
24 | 25 |
26 | {{ form.category(class="form-control form-control-sm", title="category") }} 27 |
28 |
29 | 30 |
31 |
32 | 33 | 34 | {% if form.category.errors %} 35 |
36 |
37 | {% for error in form.category.errors %} 38 | {{ error }} 39 | {% endfor %} 40 |
41 |
42 | {% endif %} 43 | 44 |
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 | avatar 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 |

25 | 26 | #{{ ticket.id_zfill }} | {{ ticket.title }} 27 | 28 |

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 |
18 | 19 | 20 |
21 | {{ form.hidden_tag() }} 22 | 23 | 24 |
25 |
26 | {{ _('Please add new users credentials.') }} 27 |
28 |
29 | 30 |
31 |
32 | {{ _('Group Name') }} 33 |
34 |
35 | {{ form.group_name(class="form-control form-control-sm") }} 36 | {% if form.group_name.errors %} 37 | 38 | {% for error in form.group_name.errors %} 39 | [{{ error }}] 40 | {% endfor %} 41 | 42 | {% endif %} 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 | 53 |
54 |
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 |
27 | {{ form.hidden_tag() }} 28 | 29 | 30 |
31 | 34 |
35 | {{ form.department_category(class="form-control form-control-sm", id="autocomplete-department_category") }} 36 |
37 |
38 | {{ form.submit }} 39 |
40 |
41 | {% if form.department_category.errors %} 42 |
43 | {% for error in form.department_category.errors %} 44 | {{ error }} 45 | {% endfor %} 46 |
47 | {% endif %} 48 |
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 |
17 |
18 | 19 |
20 |
21 | {{ _('Type name and pick from drop down list to assign ticket. ') }} 22 | 23 | Available users 24 | 25 |
26 |
27 |
32 | {{ form.hidden_tag() }} 33 |
34 | 36 |
37 | {{ form.username(class="form-control form-control-sm", id="autocomplete-username") }} 38 | {% if form.username.errors %} 39 |
40 | {% for error in form.username.errors %} 41 | {{ error }} 42 | {% endfor %} 43 |
44 | {% endif %} 45 |
46 | 47 |
48 |
49 |
50 | {{ form.submit }} 51 |
52 |
53 |
54 |
55 |
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 |
14 |
15 |
16 |
17 | {{ form.hidden_tag() }} 18 | {{ form.id() }} 19 |
20 |
21 | {{ _('Are you sure you want to delete the group:') }} 22 |
23 |
24 |
25 |
26 | {{ group_details.group_name }} 27 |
28 |
29 |
30 |
31 | {{ _('Please enter your password to delete group.') }} 32 |
33 |
34 |
35 |
36 | {{ form.password(class="form-control form-control-sm") }} 37 | {%- if form.password.errors -%} 38 | 39 | {%- for error in form.password.errors -%} 40 | [{{ error }}]
41 | {%- endfor -%} 42 |
43 | {%- endif -%} 44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
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 | 30 | {% endif %} 31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 |

{{ _('Statistics') }}

42 |
43 |
44 |
45 | 46 | {% for id in ids %} 47 |
48 |
49 |
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 |
16 |
17 | 18 |
19 | {{ form.hidden_tag() }} 20 |
21 | 22 | {{ form.username(class="form-control") }} 23 |
24 | {% if form.username.errors %} 25 |
26 | {% for error in form.username.errors %} 27 | {{ error }} 28 | {% endfor %} 29 |
30 | {% endif %} 31 |
32 | 33 | {{ form.password(class="form-control") }} 34 |
35 | {% if form.password.errors %} 36 |
37 | {% for error in form.password.errors %} 38 | {{ error }} 39 | {% endfor %} 40 |
41 | {% endif %} 42 | 43 |
44 | 45 | {{ form.remember_me }} 46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 |
57 |
58 |
59 | reset password 60 |
61 |
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 |
41 | {{ form.hidden_tag() }} 42 |

43 | {{ _('Please enter group name.') }} 44 |

45 |
46 |
47 | {{ _('Group Name') }} 48 | 49 |
50 |
51 | {{ form.group_name(size=(24)) }} 52 | {% if form.group_name.errors %} 53 | 54 | {% for error in form.group_name.errors %} 55 | [{{ error }}] 56 | {% endfor %} 57 | 58 | {% endif %} 59 |
60 |
61 |
62 |
63 |
64 |
65 |
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 |
26 | {{ form.hidden_tag() }} 27 | {{ form.department_id() }} 28 |
29 | 30 |
31 | {{ form.category(class="form-control form-control-sm", placeholder="category") }} 32 |
33 |
{{ form.submit() }} 34 |
35 |
36 | {% if form.category.errors %} 37 |
38 |
39 | {% for error in form.category.errors %} 40 | {{ error }} 41 | {% endfor %} 42 |
43 |
44 | {% endif %} 45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |

{{ _('Existing Categories') }}

53 |
54 | {% for c in categories %} 55 |
56 |
57 | {{ c.category }}  58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
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 |
19 |
20 | {{ form.hidden_tag() }} 21 | {{ form.id() }} 22 |
23 |
24 | {{ _('Are you sure you want to delete the following user? This may impact relational query look ups.') }} 25 |
26 |
27 | 28 |
29 |
30 | {{ _('Username') }} 31 |
32 |
33 | {{ user_details.username }} 34 |
35 |
36 |
37 |
38 | {{ _('Name') }} 39 |
40 |
{{ user_details.name }}
41 |
42 |
43 |
44 | {{ _('Email') }} 45 |
46 |
47 | {{ user_details.email }} 48 |
49 |
50 |
51 |
52 | {{ _('Enter password') }} 53 |
54 |
55 | {{ form.password(class="form-control form-control-sm") }} 56 | {% if form.password.errors %} 57 | 58 | {% for error in form.password.errors %} 59 | [{{ error }}] 60 | {% endfor %} 61 | 62 | {% endif %} 63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 |
71 |
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 |
6 |
7 |
12 | {{ form.hidden_tag() }} 13 | 14 | 15 |
16 | 19 |
20 | {{ form.content(class="form-control") }} 21 |
22 |
23 | {% if form.content.errors %} 24 |
25 | {% for error in form.content.errors %} 26 | {{ error }} 27 | {% endfor %} 28 |
29 | {% endif %} 30 | 31 | {% if form.uploads %} 32 | {{ form.uploads }} 33 | {% endif %} 34 | 35 | 36 |
37 | 42 |
43 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | {{ form.file(class="form-control-file border-0") }} 53 |
54 |
55 | 56 | {{ form.status(class="form-control form-control-xs") }} 57 |
58 |
59 | 60 | {{ form.priority(class="form-control form-control-xs") }} 61 |
62 |
63 | 64 | {{ form.hours(class="form-control form-control-xs") }} 65 |
66 | 67 |
68 |
69 |
70 | 71 | {% if ticket %} 72 | {% if g.user.is_admin or (ticket.user == g.user) or (ticket.assigned_id == g.user.id) %} 73 | 74 | {% if ticket.current_status.status != 'Closed' %} 75 | {{ form.submit_close(class="btn btn-light btn-outline-danger btn-sm") }}    76 | {% endif %} 77 | {% endif %} 78 | {% endif %} 79 | 80 | {{ form.submit(class="btn btn-primary btn-sm") }} 81 | 82 |
83 |
84 |
85 |
86 |
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 |
18 |
19 | 20 |
21 | {{ form.hidden_tag() }} 22 |
23 |
24 | {{ _('Field') }} 25 |
26 |
27 | {{ _('Value') }} 28 |
29 |
30 | {% set insert_button = False %} 31 | {%- for field in form if field.widget.input_type != 'hidden' and field.widget.input_type != 'submit' -%} 32 |
33 | {{ field.label(class="col-3") }} 34 | {% if field.type == 'BooleanField' %} 35 |
36 | {% else %} 37 |
38 | {% endif %} 39 | 40 | {{ field(class="form-control form-control-sm") }} 41 | {%- if field.name == 'allowed_extensions' -%} 42 | {{ _('This must be a comma delimited list.') }} 43 | {% endif %} 44 | {%- if field.errors -%} 45 | 46 | {%- for error in field.errors -%} 47 | {{ error }}. 48 | {%- endfor -%} 49 | 50 | {%- endif -%} 51 | 52 | {%- if field.name == "mail_ascii_attachments" -%} 53 | {% set insert_button = True %} 54 | {%- endif -%} 55 |
56 |
57 | {%- if insert_button -%} 58 |
59 |
60 | Test 61 | Email 62 |
63 |
64 | {%- endif -%} 65 | {%- endfor -%} 66 |
67 |
68 | {{ form.submit }} 69 |
70 |
71 |
72 |
73 | 74 |
75 |
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. --------------------------------------------------------------------------------