diff --git a/app/__init__.py b/app/__init__.py index f8a56ee..d8c7e6d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -68,6 +68,9 @@ app.register_blueprint(smanager_blueprint, url_prefix='/smanager') from .dmanager import dmanager as dmanager_blueprint app.register_blueprint(dmanager_blueprint, url_prefix='/dmanager') +from .livesupport import livesupport as livesupport_blueprint +app.register_blueprint(livesupport_blueprint, url_prefix='/support') + from .uinvoice import uinvoice as uinvoice_blueprint app.register_blueprint(uinvoice_blueprint, url_prefix='/uinvoice') diff --git a/app/livesupport/__init__.py b/app/livesupport/__init__.py new file mode 100644 index 0000000..e991060 --- /dev/null +++ b/app/livesupport/__init__.py @@ -0,0 +1,3 @@ +from flask import Blueprint +livesupport = Blueprint('livesupport', __name__) +from . import routes diff --git a/app/livesupport/forms.py b/app/livesupport/forms.py new file mode 100644 index 0000000..ee6f96f --- /dev/null +++ b/app/livesupport/forms.py @@ -0,0 +1,27 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField +from flask_pagedown.fields import PageDownField +from wtforms import validators, ValidationError +from wtforms.fields.html5 import EmailField, DecimalRangeField + +from .. import db + +class OrderForm(FlaskForm): + region_choices = [(1, 'Plovdiv, Bulgaria'), (2, 'International Space Station')] + region = SelectField('Region:', choices=region_choices, coerce=int) + + recipe_choices = [(1, 'RootVPS')] + recipe = SelectField('Type:', choices=recipe_choices, coerce=int) + + cpu = DecimalRangeField('Processor Cores', default=2) + memory = DecimalRangeField('Memory', default=2048) + storage = DecimalRangeField('Storage', default=20) + + alias = StringField('Name:', [validators.Regexp(message='ex.: myservice1.com, myservice2.local', regex='^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$'), validators.Length(6,64)]) + + submit = SubmitField('DEPLOY') + +class MessageForm(FlaskForm): + line = PageDownField('Enter your message...', validators=[validators.DataRequired()]) + submit = SubmitField('Submit') + diff --git a/app/livesupport/routes.py b/app/livesupport/routes.py new file mode 100644 index 0000000..5f5c0e2 --- /dev/null +++ b/app/livesupport/routes.py @@ -0,0 +1,83 @@ +from flask import render_template, abort, redirect, url_for, abort, flash, request, current_app, make_response, g +from flask_login import login_required, login_user, logout_user, current_user +from flask_sqlalchemy import get_debug_queries + +from . import livesupport +from .forms import MessageForm +from .. import db +from ..email import send_email +from ..models import User, Permission, SupportTopic, SupportLine, contact_proxmaster + +import base64 +from datetime import date, time, datetime +from dateutil.relativedelta import relativedelta + +@livesupport.after_app_request +def after_request(response): + for query in get_debug_queries(): + if query.duration >= current_app.config['SLOW_DB_QUERY_TIME']: + current_app.logger.warning('Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n' % (query.statement, query.parameters, query.duration, query.context)) + return response + +#SUPPORT +@livesupport.route("/support", methods=['GET']) +@login_required +def support_list(): + """ general enquiry and list all open support tasks """ + cuser = current_user + form = MessageForm() + + alltopics = cuser.inv_topics.all() + return render_template('livesupport/support_list.html', form=form, inv_topics=alltopics) + +@livesupport.route("/support//", methods=['GET', 'POST']) +@login_required +def support(topic): + """ block item for support chatbox. invoked from vdc_pool or supportlist """ + cuser = current_user + form = MessageForm() + + if request.method == "GET": + support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first() + if support_topic == None: + class EmptySupport(): + hashtag=str(topic) + timestamp=datetime.utcnow() + support_topic = EmptySupport() + return render_template('livesupport/support_item.html', form=form, support=support_topic) + else: + if support_topic.user_id != cuser.pid: + abort(403) #TODO: hidden 403. there is a topic like that but its not yours! + else: + #topic is yours. show it. + return render_template('livesupport/support_item.html', form=form, support=support_topic) + + if request.method == "POST" and form.validate_on_submit(): + support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first() + if support_topic == None: + #no topic. create one? + if cuser.inv_topics.all() != []: + #check if other topics exist, and ratelimit + last_topic = cuser.inv_topics.order_by(SupportTopic.timestamp.desc()).first() + now = datetime.utcnow() + time_last_topic = last_topic.timestamp + expiry = time_last_topic + relativedelta(time_last_topic, minutes=+5) + if now < expiry: + flash('ratelimit. try again later') + return redirect(url_for('livesupport.support_list')) + #create new topic + new_topic = SupportTopic(user_id=cuser.pid, hashtag=str(topic)) + db.session.add(new_topic) + new_line = SupportLine(topic_id=new_topic.pid, line=str(form.line.data)) + db.session.add(new_line) + + else: + if support_topic.user_id == cuser.pid: + new_line = SupportLine(topic_id=support_topic.pid, line=form.line.data) + db.session.add(new_line) + else: + abort(403) #TODO: hidden 404 + + db.session.commit() + return redirect(url_for('livesupport.support_list')) + diff --git a/app/models.py b/app/models.py index 947b2b5..d731759 100644 --- a/app/models.py +++ b/app/models.py @@ -441,7 +441,7 @@ class SupportTopic(db.Model): class SupportLine(db.Model): __tablename__ = 'support_line' pid = db.Column(db.Integer, primary_key=True) - topic_id = db.Column(db.ForeignKey('support_topic.pid')) + topic_id = db.Column(db.ForeignKey('support_topic.pid')) #FK line = db.Column(db.Unicode) timestamp = db.Column(db.DateTime(), default=datetime.utcnow) diff --git a/app/panel/forms.py b/app/panel/forms.py index ee6f96f..56ab715 100644 --- a/app/panel/forms.py +++ b/app/panel/forms.py @@ -21,7 +21,4 @@ class OrderForm(FlaskForm): submit = SubmitField('DEPLOY') -class MessageForm(FlaskForm): - line = PageDownField('Enter your message...', validators=[validators.DataRequired()]) - submit = SubmitField('Submit') diff --git a/app/panel/routes.py b/app/panel/routes.py index 2ec384c..4fd1568 100644 --- a/app/panel/routes.py +++ b/app/panel/routes.py @@ -6,7 +6,7 @@ from . import panel from .forms import OrderForm, MessageForm from .. import db from ..email import send_email -from ..models import User, Permission, Recipe, Order, Server, Deployment, Service, Region, Address, Domain, SupportTopic, SupportLine, contact_proxmaster +from ..models import User, Permission, Recipe, Order, Server, Deployment, Service, Region, Address, Domain, contact_proxmaster import base64 from datetime import date, time, datetime @@ -99,63 +99,3 @@ def dashboard(user_pid): continue return render_template('panel/dashboard.html', sys_regions=sys_regions, inv_deployments=inv_deployments, inv_services=inv_services, inv_domains=inv_domains, inv_addresses=inv_addresses, rrd=rrd, status=statuses, warnflag=warnflag, regions=regions) -#SUPPORT -@panel.route("/support", methods=['GET']) -@login_required -def list_support(): - """ general enquiry and list all open support tasks """ - cuser = current_user - alltopics = cuser.inv_topics.all() - return render_template('panel/support_list.html', inv_topics=alltopics) - -@panel.route("/support//", methods=['GET', 'POST']) -@login_required -def support(topic): - """ block item for support chatbox. invoked from vdc_pool or supportlist """ - cuser = current_user - form = MessageForm() - - if request.method == "GET": - support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first() - if support_topic == None: - class EmptySupport(): - hashtag=str(topic) - - support_topic = EmptySupport() - return render_template('panel/support_item.html', form=form, support=support_topic) - else: - if support_topic.user_id != cuser.pid: - abort(403) #TODO: hidden 403. there is a topic like that but its not yours! - else: - #topic is yours. show it. - return render_template('panel/support_item.html', form=form, support=support_topic) - - if request.method == "POST" and form.validate_on_submit(): - support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first() - if support_topic == None: - #no topic. create one? - if cuser.inv_topics.all() != []: - #check if other topics exist, and ratelimit - last_topic = cuser.inv_topics.order_by(SupportTopic.timestamp.desc()).first() - now = datetime.utcnow() - time_last_topic = last_topic.timestamp - expiry = time_last_topic + relativedelta(time_last_topic, minutes=+5) - if now < expiry: - flash('ratelimit. try again later') - return redirect(url_for('panel.dashboard')) - #create new topic - new_topic = SupportTopic(user_id=cuser.pid, hashtag=str(topic)) - db.session.add(new_topic) - new_line = SupportLine(topic_id=new_topic.pid, line=str(form.line.data)) - db.session.add(new_line) - - else: - if support_topic.user_id == cuser.pid: - new_line = SupportLine(topic_id=support_topic.pid, line=form.line.data) - db.session.add(new_line) - else: - abort(403) #TODO: hidden 404 - - db.session.commit() - return redirect(url_for('panel.dashboard')) - diff --git a/app/settings/forms.py b/app/settings/forms.py index 16f07d2..c205426 100644 --- a/app/settings/forms.py +++ b/app/settings/forms.py @@ -24,7 +24,7 @@ class EditProfileForm(FlaskForm): org_account = BooleanField('This is a business account.') org_companyname = StringField('Company Name:') org_regaddress = StringField('Company Address:') - org_responsible = StringField('Accountable Person (optional):') + org_responsible = StringField('Accountable Person:') org_vatnum = StringField('VAT Number:') twofactor = BooleanField('Enable 2-factor authentication') submit = SubmitField('Update') diff --git a/app/static/css/style.css b/app/static/css/style.css index b93e887..d81eb44 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -200,7 +200,7 @@ a:active { } .form { - max-width: 500px; + max-width: 660px; } .padding-left-32 { diff --git a/app/static/livesupport/chat.css b/app/static/livesupport/chat.css new file mode 100644 index 0000000..a13d378 --- /dev/null +++ b/app/static/livesupport/chat.css @@ -0,0 +1,99 @@ +.mytext{ + border:0;padding:10px;background:whitesmoke; +} +.text{ + width:75%;display:flex;flex-direction:column; +} +.text > p:first-of-type{ + width:100%;margin-top:0;margin-bottom:auto;line-height: 13px;font-size: 12px; +} +.text > p:last-of-type{ + width:100%;text-align:right;color:silver;margin-bottom:-7px;margin-top:auto; +} +.text-l{ + float:left;padding-right:10px; +} +.text-r{ + float:right;padding-left:10px; +} +.avatar{ + display:flex; + justify-content:center; + align-items:center; + width:25%; + float:left; + padding-right:10px; +} +.macro{ + margin-top:5px;width:85%;border-radius:5px;padding:5px;display:flex; +} +.msj-rta{ + float:right;background:whitesmoke; +} +.msj{ + float:left;background:white; +} +.frame{ + background:#e0e0de; + height:450px; + overflow:hidden; + padding:0; +} +.frame > div:last-of-type{ + position:absolute;bottom:0;width:100%;display:flex; +} +body > div > div > div:nth-child(2) > span{ + background: whitesmoke;padding: 10px;font-size: 21px;border-radius: 50%; +} +body > div > div > div.msj-rta.macro{ + margin:auto;margin-left:1%; +} +ul { + width:100%; + list-style-type: none; + padding:18px; + position:absolute; + bottom:47px; + display:flex; + flex-direction: column; + top:0; + overflow-y:scroll; +} +.msj:before{ + width: 0; + height: 0; + content:""; + top:-5px; + left:-14px; + position:relative; + border-style: solid; + border-width: 0 13px 13px 0; + border-color: transparent #ffffff transparent transparent; +} +.msj-rta:after{ + width: 0; + height: 0; + content:""; + top:-5px; + left:14px; + position:relative; + border-style: solid; + border-width: 13px 13px 0 0; + border-color: whitesmoke transparent transparent transparent; +} +input:focus{ + outline: none; +} +::-webkit-input-placeholder { /* Chrome/Opera/Safari */ + color: #d4d4d4; +} +::-moz-placeholder { /* Firefox 19+ */ + color: #d4d4d4; +} +:-ms-input-placeholder { /* IE 10+ */ + color: #d4d4d4; +} +:-moz-placeholder { /* Firefox 18- */ + color: #d4d4d4; +} + diff --git a/app/static/livesupport/chat.js b/app/static/livesupport/chat.js new file mode 100644 index 0000000..d8884ee --- /dev/null +++ b/app/static/livesupport/chat.js @@ -0,0 +1,83 @@ +var me = {}; +me.avatar = "https://lh6.googleusercontent.com/-lr2nyjhhjXw/AAAAAAAAAAI/AAAAAAAARmE/MdtfUmC0M4s/photo.jpg?sz=48"; + +var you = {}; +you.avatar = "https://a11.t26.net/taringa/avatares/9/1/2/F/7/8/Demon_King1/48x48_5C5.jpg"; + +function formatAMPM(date) { + var hours = date.getHours(); + var minutes = date.getMinutes(); + var ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + minutes = minutes < 10 ? '0'+minutes : minutes; + var strTime = hours + ':' + minutes + ' ' + ampm; + return strTime; +} + +//-- No use time. It is a javaScript effect. +function insertChat(who, text, time){ + if (time === undefined){ + time = 0; + } + var control = ""; + var date = formatAMPM(new Date()); + + if (who == "me"){ + control = '
  • ' + + '
    ' + + '
    ' + + '
    ' + + '

    '+ text +'

    ' + + '

    '+date+'

    ' + + '
    ' + + '
    ' + + '
  • '; + }else{ + control = '
  • ' + + '
    ' + + '
    ' + + '

    '+text+'

    ' + + '

    '+date+'

    ' + + '
    ' + + '
    ' + + '
  • '; + } + setTimeout( + function(){ + $("ul").append(control).scrollTop($("ul").prop('scrollHeight')); + }, time); + +} + +function resetChat(){ + $("ul").empty(); +} + +$(".mytext").on("keydown", function(e){ + if (e.which == 13){ + var text = $(this).val(); + if (text !== ""){ + insertChat("me", text); + $(this).val(''); + } + } +}); + +$('body > div > div > div:nth-child(2) > span').click(function(){ + $(".mytext").trigger({type: 'keydown', which: 13, keyCode: 13}); +}) + +//-- Clear Chat +resetChat(); + +//-- Print Messages +insertChat("me", "Hello Tom...", 0); +insertChat("you", "Hi, Pablo", 1500); +insertChat("me", "What would you like to talk about today?", 3500); +insertChat("you", "Tell me a joke",7000); +insertChat("me", "Spaceman: Computer! Computer! Do we bring battery?!", 9500); +insertChat("you", "LOL", 12000); + + +//-- NOTE: No use time on insertChat. diff --git a/app/templates/livesupport/support_item.html b/app/templates/livesupport/support_item.html new file mode 100644 index 0000000..ef1b5e4 --- /dev/null +++ b/app/templates/livesupport/support_item.html @@ -0,0 +1,39 @@ +{% block support_item %} +{% if support.inv_lines != [] %} +
    +
      +
      +
      +
      + +
      +
      + +
      +
      + +
      + +
      + +
      + {% for line in support.inv_lines %} +
      {{ line.topic.owner.email }} {{ moment(line.timestamp).format('lll') }}
      + {{ line.line }}
      + {% endfor %} + {% endif %} + +
      + {{ form.line | safe }} + {% for error in form.line.errors %} + {{ error }}
      + {% endfor %} + + {{ form.csrf_token() }} + {{ form.submit }} +
      + +{% endblock %} + diff --git a/app/templates/panel/support_list.html b/app/templates/livesupport/support_list.html similarity index 79% rename from app/templates/panel/support_list.html rename to app/templates/livesupport/support_list.html index e60fb0a..a4ee8b7 100644 --- a/app/templates/panel/support_list.html +++ b/app/templates/livesupport/support_list.html @@ -19,18 +19,19 @@ $('a[data-toggle="tooltip"]').tooltip({
      {% if inv_topics != [] %} + {% for support in inv_topics %}
      -
      Support
      +
      {{ support.hashtag }}

      -

      - {% for support in inv_topics %} +
      {% include "panel/support_item.html" %} +
      +
      +
      +
      {% endfor %} -
      -
      - - + {% endif %}
      diff --git a/app/templates/nav.html b/app/templates/nav.html index 63349f7..93c137c 100644 --- a/app/templates/nav.html +++ b/app/templates/nav.html @@ -52,7 +52,7 @@
    • Transactions
    • Profile
    • -
    • Live Chat
    • +
    • Support
    • Logout
    • diff --git a/app/templates/panel/support_item.html b/app/templates/panel/support_item.html deleted file mode 100644 index 0f37073..0000000 --- a/app/templates/panel/support_item.html +++ /dev/null @@ -1,24 +0,0 @@ -{% block support_item %} -
      - -
      - -
      - {% for line in inv_lines %} - {{ line.timestamp }} - {{ line.line }}
      - {% endfor %} - -
      - {{ form.line | safe }} - {% for error in form.line.errors %} - {{ error }}
      - {% endfor %} - - {{ form.csrf_token() }} - {{ form.submit }} -
      - -{% endblock %}