1
\$\begingroup\$

I build a small web-app using flask for my parents to text their customers about promotions. I am using Twilio API. I just finished the myapp.com/new-campaign page and I would like to have some suggestions as it's my first full stack applicaton as I just added a new feature: estimated cost of the campaign (coût) enter image description here

What are the requirements of this page:

  • List: Select a list of clients (I don't think we will ever have more than 2 lists, one for testing and 1 for customers)
  • Coût: total estimated cost for the campaign
  • Message: message to send

How is the cost calculated?

  1. number of recipient (which lists I am using)
  2. message length (basically, between 0 and 160 characters it's segment 1 ; 160 to 320 it's segment 2 and so on). Segment 1 is $0.06, segment 2 is $0.06 * 2 and so on.

My goal was to give a direct estimate of the total cost of a campaign.

  1. When the user select a new customer list: making a back-end call to get the length of the list
  2. For the message input, I told myself that it would be stupid to make a back-end call everytime I am adding a remove a letter in the textarea. I just to need to recalculate the cost when it's reaching a new segment base on number of characters. So I am using JS for that and making an AJAX call only when I am in a new segment (upgrade or downgrade)

message-calculator.js

  var inputMessageLength = 0;
    var currentSegment = 1;
    var SelectedCustomerList;
    
    // on load of the page 
    $(document).ready(function () {
      updateList(getSelectedCustomersList());
      updateCost();
    });
    
    // call the get_cost_estimation to update the price 
    function updateCost() {
      $.ajax({
        url: "/get_cost_estimation",
        type: "POST",
        data: JSON.stringify({
          input_length: inputMessageLength,
          selected_list: SelectedCustomerList,
        }),
        contentType: "application/json; charset=utf-8",
        dataType: "json",
      }).done(function (data) {
        var newCost = data['estimated_cost'] + ' ' + data['currency'];
        document.getElementById("total-cost-value").innerHTML = newCost
      });
    }
    
    function updateList(NewSelectedCustomerList) {
      SelectedCustomerList = NewSelectedCustomerList;
      updateCost();
    }
    
    function getSelectedCustomersList() {
      return $("input[name=list]:checked").val();
    }
    
    function updateSegmentNumber(newSegmentNumber){
      currentSegment = newSegmentNumber;
    }
    
    // caled everytime the user make a change in the message input area 
    function updateMessage(body, SegmentCharactersLimit) {
      var messageContent = body.value;
      inputMessageLength = messageContent.length;
      updateSegmentMessage(inputMessageLength, SegmentCharactersLimit);
      
      var newSegment = Math.ceil(inputMessageLength / SegmentCharactersLimit)
      if(newSegment != currentSegment){
        updateSegmentNumber(newSegment);
        updateCost();
      }
    }
    
    // alert message displayed to tell the client that he already reached the limit of segment 1
    function updateSegmentMessage(inputMessageLength, SegmentCharactersLimit) {
      if (inputMessageLength > SegmentCharactersLimit) {
        document.getElementById("message-cost-alert").style.display = "block";
      } else {
        document.getElementById("message-cost-alert").style.display = "none";
      }
    }

views.py

    from config.settings import COST_PER_SEGMENT, MAX_CARACTERS_PER_SEGMENT 
    ...
    def total_cost_estimation(quantity, input_length):
        number_of_segments = math.ceil(input_length / MAX_CARACTERS_PER_SEGMENT)
        if number_of_segments < 1: 
            number_of_segments = 1
        estimated_cost_per_sms = number_of_segments * COST_PER_SEGMENT
        total_estimated_cost = estimated_cost_per_sms * quantity
        print(estimated_cost_per_sms)
        print(quantity)
        return round(total_estimated_cost, 2)
    
    
    @user.route("/get_cost_estimation", methods=['GET', 'POST'])
    def get_cost_estimation():
        currency = '&euro;'
        input_length = request.json['input_length']
        selected_list = request.json['selected_list']  
        if selected_list == 'test-list':
            count = mongo.db[customers_test].count()
        else:
            count = mongo.db[customers_production].count()
        estimated_cost = str(total_cost_estimation(count, input_length))
        return jsonify(
            estimated_cost=estimated_cost,
            currency=currency,
        )
    
    @user.route("/campaigns", methods=['GET'])
    @login_required
    def campaigns():
        return render_template('campaigns.html', currency='€', cost_per_sms = 0, max_caracters = MAX_CARACTERS_PER_SEGMENT)
    ...
 

campaigns.html

<div class="container">
    <h2>Choisissez votre message</h2>
    <form name="sms-form" action="/launch-campaign" method="POST">
        <div class="form-group row">
          </div>
          <div class="form-group row">
          </div>
          <fieldset class="form-group">
            <div class="row">
              <legend class="col-form-label col-sm-2 pt-0">Liste</legend>
              <div class="col-sm-10">
                <div class="form-check">
                  <input class="form-check-input" onclick="updateList('test-list')" type="radio" name="list" id="gridRadios1" value="test-list" checked>
                  <label class="form-check-label" for="gridRadios1">
                    Test
                  </label>
                </div>
                <div class="form-check">
                  <input class="form-check-input" onclick="updateList('production-list')" type="radio" name="list" id="gridRadios2" value="production-list">
                  <label class="form-check-label" for="gridRadios2">
                    Clients
                  </label>
                </div>
            </div>
          </fieldset>


          <fieldset class="form-group">
           ...
          </fieldset>


          <div class="form-group row">
            <div class="col-sm-2">Message</div>
            <div class="col-sm-10">
              <div class="form-check">
                <textarea name="body" oninput="updateMessage(this, {{max_caracters}})" id="form10" class="md-textarea form-control" rows="3"></textarea>
          </div>

          <div id="message-cost-alert"class="alert alert-warning alert-dismissible fade show">
            <strong>Attention!</strong> Votre message dépasse le nombre de {{max_caracters}} caracters
            <button type="button" class="close" data-dismiss="alert">&times;</button>
          </div>

          <div class="form-group row">
            <div class="col-sm-10">
                <br>
              <button type="submit" class="btn btn-primary" onclick="return confirm('Are you sure?')">Envoyer</button>
            </div>
          </div>
    </form>
</div>
\$\endgroup\$
1
\$\begingroup\$

Typos

caled everytime -> called every time

MAX_CARACTERS_PER_SEGMENT -> MAX_CHARACTERS_PER_SEGMENT

max_caracters -> max_characters

And my first language is not French, but shouldn't

Votre message dépasse le nombre de {{max_caracters}} caracters

be

Votre message dépasse le nombre de {{max_characters}} caractères

?

Language consistency

Your UI is in French - great; but then why do you show

return confirm('Are you sure?')

?

DRY indexing

        if selected_list == 'test-list':
            count = mongo.db[customers_test].count()
        else:
            count = mongo.db[customers_production].count()

can be something like

if selected_list == 'test-list':
    table = customers_test
else:
    table = customers_production
count = mongo.db[table].count()

Immutable methods

Since the methods on a route are not expected to change, I find

methods=['GET', 'POST']

to be better-expressed as a tuple:

methods=('GET', 'POST')
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.