ในช่วงไม่กี่เดือนที่ผ่านมา ฉันใช้เวลามากมายไปกับบริการ "Pinecast" ซึ่งเป็นบริการโฮสต์พอดแคสต์ของฉัน การโฮสต์พอดแคสต์ค่อนข้างตรงไปตรงมา: ฟังก์ชั่นหลักนั้นไม่ซับซ้อนมากนัก เชื่อมต่อบัคเก็ต S3 เข้ากับฟีด RSS เท่านี้คุณก็พร้อมแล้ว ผู้ใช้บางรายต้องการคุณสมบัติมากกว่าแค่โฮสติ้ง: พอดแคสต์ต้องการเว็บไซต์ การวิเคราะห์ที่ชัดเจนเป็นสิ่งจำเป็นในการทำความเข้าใจผู้ชม และการปรับปรุงคุณภาพชีวิตมักจำเป็นเพื่อป้องกันไม่ให้ลูกค้าเปลี่ยนมาใช้คู่แข่ง

ฉันใช้งานพอร์ทัลสนับสนุนมาระยะหนึ่งแล้ว แต่ฉันต้องการรับคำติชมโดยตรงจากผู้ใช้ของฉันเกี่ยวกับสิ่งที่พวกเขาต้องการ ฉันสามารถให้แนวคิดเกี่ยวกับคุณลักษณะที่ฉันต้องการให้พวกเขาได้ แต่ฉันอาจพลาดบางสิ่งบางอย่างไปได้ง่าย ฉันยังต้องการรวบรวมลำดับความสำคัญ: คุณลักษณะที่ผู้ใช้รายหนึ่งต้องการมีความสำคัญน้อยกว่าคุณลักษณะที่ผู้ใช้จำนวนมากต้องการ ฉันต้องการวิธีให้ผู้ใช้โหวตฟีเจอร์ที่พวกเขาต้องการมากที่สุด

ขณะที่อ่านกล่องจดหมายของฉัน ฉันเห็นโพสต์เกี่ยวกับบริการ "Canny" Canny ตรงกับที่ฉันต้องการ: วิธีที่สะอาดและง่ายต่อการผสานรวมเพื่อให้ผู้ใช้ของฉันโพสต์และลงคะแนนสำหรับคุณสมบัติต่างๆ เมื่อดูจากแผนแล้ว ก็ดูเหมือนว่าจะมีราคาไม่แพงเพียงพอเช่นกัน $2/เดือน สำหรับแผนเริ่มต้น? แน่นอน! ฉันลงทะเบียนและเริ่มการบูรณาการ

ไม่มีอาหารกลางวันฟรี

เช่นเดียวกับของเล่นใหม่อื่นๆ ปัจจัยด้านว้าวก็หมดไปอย่างรวดเร็ว ประการแรก การบูรณาการ SSO (ความสามารถในการให้ผู้ใช้ของคุณมีบัญชีบน Canny โดยอัตโนมัติ) ไม่มีตัวอย่างโค้ด Python “ง่ายพอ” ฉันคิด จนกระทั่งฉันตระหนักว่ามันเกี่ยวข้องกับ crypto

ต่างจากภาษาอื่น ๆ ส่วนใหญ่ ไม่มีไลบรารี่ crypto ที่เข้าถึงได้ง่ายสำหรับ Python ไป ค้นหา "python crypto Library" คุณจะสังเกตเห็นผลลัพธ์แรก “pycrypto” ผลการค้นหานี้จะทำให้คุณเข้าใจผิด เนื่องจาก pycrypto ไม่รองรับ Python 3.4 ขึ้นไป (Pinecast ทำงานบน 3.5) ฉันพยายามย้ายตัวอย่างโค้ดอื่นๆ เพื่อทำงานกับไลบรารี การเข้ารหัส ซึ่งได้รับการสนับสนุนที่ดีกว่ามาก แต่ท้ายที่สุดฉันก็ล้มเหลว หลังจากที่กลับไปกลับมากับวิศวกรที่ Canny เราก็ได้เวอร์ชันที่ใช้งานได้ ที่กล่าวว่าฉันยังไม่เข้าใจว่าเหตุใดหรือทำงานอย่างไร

หลังจากฝังวิดเจ็ตแล้ว ฉันประสบปัญหาเพิ่มเติม ประการแรก พวกเขาต้องการชื่อผู้ใช้ ฉันไม่รู้ว่าบริการประเภทไหนที่ถามชื่อจริงของผู้ใช้เมื่อพวกเขาสมัครใช้งาน แต่ Pinecast ไม่ใช่หนึ่งในนั้น ฉันลงเอยด้วย การเขียนฟังก์ชันแฮชสุดไร้สาระ เพื่อสร้างชื่อไร้สาระจากอีเมลของผู้ใช้

ปัญหาอีกประการหนึ่งคือความสามารถในการปรับแต่งได้ แม้จะอนุญาตให้คุณเปลี่ยนป้ายกำกับของช่องแบบฟอร์มในวิดเจ็ตได้ แต่ข้อความ "สร้างโพสต์" ไม่สามารถเปลี่ยนเป็น "แนะนำคุณลักษณะ" หรือสิ่งที่คล้ายกันได้ “โพสต์” ไม่ใช่คำที่ชัดเจนนักในบริบทของหน้าคำติชมของ Pinecast

วิดเจ็ตยังโหลดช้ามากอีกด้วย หลังจากมาถึงหน้าเว็บที่ฉันสร้างขึ้น วิดเจ็ต Canny ใช้เวลาเกือบสามวินาทีบนอินเทอร์เน็ตเคเบิลของฉันจึงจะปรากฏ ไม่มีตัวแสดงการโหลด ไม่มีข้อความโหลด มีเพียงช่องว่างขนาดใหญ่ ปัญหาในบอร์ดคำขอคุณลักษณะของตนเอง ถูกเปิดขึ้นในเดือนมีนาคม

สุดท้ายนี้ เมื่ออีเมลของผู้ใช้ในโทเค็น SSO เปลี่ยนแปลง ระบบจะไม่อัปเดตอีเมลใน Canny นั่นคือหากผู้ใช้เปลี่ยนอีเมลใน Pinecast ก็จะไม่อัปเดตอีเมลใน Canny หากโพสต์ที่พวกเขาสร้างได้รับการอัปเดต พวกเขาจะไม่ได้รับการแจ้งเตือนทางอีเมล (ไม่ใช่ความผิดของตนเอง) ฉันรายงานเรื่องนี้กับ Canny แล้ว แต่พวกเขาบอกว่านี่เป็นฟีเจอร์ที่วางแผนไว้โดยไม่มีคำอธิบายเพิ่มเติมมากนัก

ทั้งหมดที่กล่าวมา วิดเจ็ตก็ทำงานได้และฉันรวบรวมข้อเสนอแนะจำนวนหนึ่งไว้พอสมควร

เมื่อฉันเรียนรู้ที่จะอ่าน

ฉันควรจะได้อ่านพิมพ์ดีด

เมื่อสิ้นสุดการทดลองใช้ฟรีหนึ่งเดือน ฉันก็พบว่าแผนบริการ $2/เดือนนั้นไม่ใช่อย่างที่ฉันคิด ในความเป็นจริง Pinecast ใช้งานไม่ได้โดยสิ้นเชิง

  • ไม่รองรับวิดเจ็ต ผู้ใช้ต้องไปที่เว็บไซต์ของ Canny
  • มันไม่ได้ให้โดเมนย่อยแก่ฉันใน Canny ดังนั้นมันจึงไม่มีแบรนด์เป็นหลัก
  • ไม่มีการรองรับ “SSO” ดังนั้นผู้ใช้ของฉันจึงต้องรักษาข้อมูลประจำตัวที่สองที่แยกจากกันใน Canny ไม่ชัดเจนว่าข้อมูลประจำตัว SSO ที่มีอยู่จะเชื่อมโยงกับข้อมูลประจำตัวใหม่หรือไม่

แผนบริการเดียวที่ใช้ได้คือตัวเลือก $49/เดือน (ชื่อ “ทีมเล็ก”) สำหรับราคานั้น คุณจะได้รับสิ่งที่คุณได้รับจากการทดลองใช้ฟรี มีตัวเลือก $19/เดือน (ชื่อน่ารักว่า “Side Project”) แต่ขาดการสนับสนุน SSO ฉันจะลองพิจารณาดู แต่การเรียกเก็บเงิน Heroku สำหรับ Pinecast นั้นไม่ได้อยู่ที่ $19/เดือนด้วยซ้ำและการมีเพจเดียวที่ผู้ใช้สามารถโหวตให้กับฟีเจอร์ต่างๆ ไม่ได้ช่วยปรับค่าใช้จ่ายให้เหมาะสมเลย

ขออภัยแคนนี่ ไม่ได้ตั้งใจให้เป็นอย่างนั้น

แผนบี

เหตุผลทั้งหมดที่ฉันเริ่ม Pinecast เมื่อหนึ่งปีที่แล้วก็เพราะฉันเค็มมากกับตัวเลือกที่ไม่ดีในโฮสต์พอดแคสต์ ฉันเป็นวิศวกร และวิศวกรก็แก้ปัญหาด้วยตัวเอง เรื่องนี้ก็คงไม่ต่างกัน

ก่อนอื่น ฉันสร้างโมเดลใน Django สำหรับฟีเจอร์ใหม่:

class UserSuggestion(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=512)
    description = models.TextField(blank=True)
    suggester = models.ForeignKey(User, related_name='user_suggestions')
    STATUS_CHOICES = (
        ('open', ugettext_lazy('Open')),
        ('closed', ugettext_lazy('Closed')),
        ('in progress', ugettext_lazy('In Progress')),
        ('complete', ugettext_lazy('Complete')),
    )
    status = models.CharField(choices=STATUS_CHOICES, default='open', max_length=max(len(k) for k, _ in STATUS_CHOICES))
    def __str__(self):
        return self.title
class UserSuggestionVote(models.Model):
    suggestion = models.ForeignKey(UserSuggestion, related_name='votes')
    voter = models.ForeignKey(User)
class UserSuggestionComment(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    suggestion = models.ForeignKey(UserSuggestion, related_name='comments')
    commenter = models.ForeignKey(User)
    body = models.TextField(blank=True)

นี่ไม่ใช่ถ้วยรางวัลแต่อย่างใด แต่มันก็ทำงานได้ดี

ต่อไป ฉันเพิ่มเส้นทางบางส่วน:

urlpatterns = [
    # ...
    url(r'^userfeedback$', views_usersuggestions.feedback, name='usersuggestions'),
    url(r'^userfeedback/new$', views_usersuggestions.feedback_new, name='usersuggestions_new'),
    url(r'^userfeedback/toggle_vote$', views_usersuggestions.feedback_toggle, name='usersuggestions_toggle'),
    url(r'^userfeedback/delete$', views_usersuggestions.feedback_delete, name='usersuggestions_delete'),
    url(r'^userfeedback/comments/(?P<id>[\w-]+)$', views_usersuggestions.feedback_comments, name='usersuggestions_comments'),
    url(r'^userfeedback/comments/(?P<id>[\w-]+)/new$', views_usersuggestions.feedback_comment, name='usersuggestions_comment'),
]

ฉันจะไม่ครอบคลุมมุมมองทั้งหมด แต่คุณสามารถ "ดูได้ด้วยตัวคุณเองบน Github"

ก่อนอื่น ฉันต้องแสดงหน้าเว็บพร้อมคำแนะนำ ง่าย!

@login_required
@require_GET
def feedback(req):
    suggestions = (
        UserSuggestion.objects
            .exclude(status='complete')
            .annotate(
                Count('votes', distinct=True),
                Count('comments', distinct=True))
            .order_by('-votes__count', '-created')
    )
    user_votes = UserSuggestionVote.objects.filter(
        voter=req.user,
        suggestion__in=suggestions,
    ).values('suggestion_id')
    ctx = {
        'was_added': 'added' in req.GET,
        'did_vote': 'voted' in req.GET,
        'did_delete': 'deleted' in req.GET,
        'suggestions': suggestions,
        'user_votes': {v['suggestion_id'] for v in user_votes},
    }
    return render(req, 'user_suggestions.html', ctx)

ขั้นแรก เราสอบถามวัตถุ UserSuggestion เมธอด annotate() ในชุดคำสั่ง Django ช่วยให้คุณสามารถแก้ไขค่าโบนัสที่มาจากฟังก์ชันเช่น COUNT และ SUM ผลลัพธ์ของแบบสอบถามนี้ทำงานได้ไม่ดีเลย สรุปยอดโหวตและแสดงความคิดเห็นยังไม่ใกล้เคียงกัน นับสอง และแสดงความวิกลจริตทั่วไป เมื่อดูเอกสารของ Django ฉันค้นพบ "อัญมณีนี้":

การรวมหลายการรวมเข้าด้วยกันด้วย annotate() จะ "ให้ผลลัพธ์ที่ไม่ถูกต้อง" เนื่องจากมีการใช้การรวมแทนแบบสอบถามย่อย

ทำไมพวกเขาถึงมีฟีเจอร์นี้ถ้ามันใช้งานไม่ได้เพราะมันอยู่นอกเหนือฉันโดยสิ้นเชิง การเพิ่ม distinct=True ให้กับออบเจ็กต์ Count จะช่วยแก้ไขปัญหาได้ แต่สำหรับ Count เท่านั้น หากคุณกำลังพยายามทำให้มันใช้งานได้กับฟังก์ชันรวมอื่น ให้ทำการค้นหาอย่างรวดเร็ว

สิ่งต่อไปที่เราทำคือค้นหาสิ่งที่ผู้ใช้โหวตให้ เราสามารถค้นหาโดยผู้ใช้และรายการข้อเสนอแนะที่เราเพิ่งดึงมา ฉันใช้ฟังก์ชัน values() เพราะฉันไม่ต้องการค้นหาข้อเสนอแนะหรือช่องผู้มีสิทธิ์เลือกตั้งอย่างเกียจคร้านอีกครั้ง

สุดท้ายนี้ ฉันโยนทั้งหมดนั้นลงในวัตถุบริบทและป้อนสิ่งนั้นให้กับวิธีการเรนเดอร์ของฉัน

ฉันคิดว่าการสร้าง UI ด้วย React แต่จริงๆ แล้วนั่นเป็นงานที่หนักมาก และจะใช้งานได้น้อยลงอย่างมากในท้ายที่สุด ฉันเลือกใช้เทมเพลต Jinja ธรรมดาๆ ที่มีการโรย POJO นี่คือสิ่งที่แทนที่วิดเจ็ตฝัง:

<form class="sidebar" action="{{ url('usersuggestions_new') }}" method="post">
  <h2>{{ _('Suggest a feature') }}</h2>
  <label>
    <span>{{ _('Title') }}</span>
    <input name="title" placeholder="{{ _('Short, descriptive title') }}">
  </label>
  <label>
    <span>{{ _('Details') }}</span>
    <textarea name="description" placeholder="{{ _('Any additional details') }}"></textarea>
  </label>
  <div style="display: flex; max-width: 300px; justify-content: flex-end;">
    <button class="btn-accent">{{ _('Submit') }}</button>
  </div>
  <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
</form>
<div>
  {% if was_added %}
    <div class="success">{{ _('Your suggestion was added!') }}</div>
  {% endif %}
  {% if did_vote %}
    <div class="success">{{ _('Your vote was updated!') }}</div>
  {% endif %}
  {% for suggestion in suggestions %}
    <div class="suggestion">
      {% if suggestion.suggester_id != user_id %}
        <form action="{{ url('usersuggestions_toggle') }}?id={{ suggestion.id }}" method="post">
          {% if suggestion.id not in user_votes %}
            <button aria-label="{{ _('Vote') }}" class="vote-btn btn-plain" title="{{ _('Vote') }}">
              <i class="icon icon-angle-up"></i>
            </button>
          {% else %}
            <button aria-label="{{ _('Unvote') }}" class="unvote-btn btn-plain" title="{{ _('Unvote') }}">
              <i class="icon icon-angle-down"></i>
            </button>
          {% endif %}
          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
        </form>
      {% else %}
        <form action="{{ url('usersuggestions_delete') }}?id={{ suggestion.id }}" method="post">
          <button aria-label="{{ _('Delete') }}" class="unvote-btn btn-plain" title="{{ _('Delete') }}">
            <i class="icon icon-trash-empty"></i>
          </button>
          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
        </form>
      {% endif %}
      <span class="vote-count">{{ suggestion.votes__count }}</span>
      <div class="title-box">
        <a href="" class="show-comments">
          {{ suggestion.title }}
          {% if suggestion.status != 'open' %}
            <span class="status-tag">{{ suggestion.status }}</span>
          {% endif %}
        </a>
      </div>
      <a class="show-comments comment-count" href="">
        {{ suggestion.comments__count }}
        <i class="icon icon-megaphone"></i>
      </a>
      <div class="comment-container" data-url="{{ url('usersuggestions_comments', id=suggestion.id) }}"></div>
    </div>
  {% endfor %}
</div>

หมายเหตุบางประการ:

  • ใช่ นั่นเป็นสไตล์แบบอินไลน์ ฉันไม่ใส่สไตล์เล็กๆ น้อยๆ ที่ทำครั้งเดียวในสไตล์ชีต นั่นเป็นวิธีที่คุณทำเรื่องยุ่งวุ่นวาย
  • ใช่ ฉันสามารถสร้างมาโครสำหรับอินพุต CSRF ได้ ฉันอาจจะควร แต่ก็ไม่ใช่ปัญหาดังกล่าว ยินดีต้อนรับคำขอดึง.
  • ฟังก์ชัน _() เป็นนามแฝงของ ugettext() ฟังก์ชัน url() เป็น wrapper รอบฟังก์ชัน reverse() ในตัวของ Django แต่รองรับ **kwargs เพราะฉันไม่ใช่คนป่าเถื่อน

ขั้นตอนสุดท้ายคือการทำให้ทุกอย่างมีการโต้ตอบกัน ฉันทิ้งสิ่งนี้ลงในแท็กสคริปต์ที่ด้านล่างของหน้า:

// Bind the links to open the comments modal
let openComments;
Array.from(document.querySelectorAll('.show-comments')).forEach(cc => {
  cc.addEventListener('click', e => {
    e.preventDefault();
    if (openComments) {
      closeComment();
    }
    openComment(
      cc.parentNode.querySelector('.comment-container') ||
      cc.parentNode.parentNode.querySelector('.comment-container')
    );
  });
});
function openComment(node) {
  openComments = node;
  openComments.classList.add('is-open');
  if (node.hasAttribute('data-is-loaded')) {
    return;
  }
  const xhr = new XMLHttpRequest();
  xhr.open('get', node.getAttribute('data-url'), true);
  xhr.send();
  xhr.onload = () => {
    node.innerHTML = xhr.responseText;
    node.setAttribute('data-is-loaded', 'true');
  };
  xhr.onerror = () => {
    node.innerHTML = '{{ _('Sorry, there was a problem loading the comments.') }}';
  };
}
function closeComment() {
  if (!openComments) {
    return;
  }
  openComments.classList.remove('is-open');
  openComments = null;
}
// Handle the close button on the comments modal
document.body.addEventListener('click', e => {
  if (!e.target.classList.contains('comment-close-btn')) {
    return;
  }
  e.preventDefault();
  closeComment();
});
// Handle comment submission
document.body.addEventListener('submit', e => {
  if (!e.target.classList.contains('comment-submit')) {
    return;
  }
  e.preventDefault();
  const xhr = new XMLHttpRequest();
  xhr.open('post', e.target.action, true);
  xhr.send(new FormData(e.target));
  const parent = e.target.parentNode;
  xhr.onload = () => openComment(parent);
  parent.innerHTML = '';
  parent.removeAttribute('data-is-loaded');
  openComment(parent);
});

ใช่ ฉันใช้สคริปต์อินไลน์ มันเหมือนกับโค้ด JavaScript แบบห่อหุ้มทั้งหมด 70 บรรทัด ต้องใช้สำเร็จรูปมากกว่าในการรับส่วนประกอบ React บนเพจมากกว่าที่ฉันสร้าง UI แบบเต็ม สู้ ๆ นะ

พื้นที่สำหรับการปรับปรุง

  • ฉันใช้พารามิเตอร์สตริงการสืบค้นสำหรับ Id ของจุดสิ้นสุดบางจุด และส่วนของเส้นทางสำหรับจุดอื่นๆ ฉันจะแปลงทั้งหมดเป็นส่วนของเส้นทาง
  • กิริยาต้องมีตัวบ่งชี้การโหลด ฉันสามารถทำได้ทั้งหมดด้วยภาพเคลื่อนไหว CSS และองค์ประกอบหลอก แต่ฉันขี้เกียจ
  • ไม่มีการแบ่งหน้าในรายการข้อเสนอแนะ Django ทำให้การแบ่งหน้าเป็นเรื่องง่าย แต่ฉันขี้เกียจ และหวังว่าฉันจะไม่ได้รับคำแนะนำมากมายจนจำเป็นต้องแบ่งหน้า
  • ไม่ใช่เรื่องง่ายที่จะดูว่าคุณกำลังส่งสำเนาซ้ำหรือไม่ แต่นั่นเป็นปัญหากับฉันมากกว่าผู้ใช้ ฉันอยากให้ผู้ใช้ส่งข้อมูลซ้ำที่ฉันรวมเข้าด้วยกัน
  • ไม่มีวิธีง่ายๆ ในการรวมคำแนะนำ ฉันจะเพิ่มฟิลด์ที่ไม่มีค่าเพื่อเชื่อมโยงข้อเสนอแนะหนึ่งไปยังอีกข้อเสนอแนะหนึ่งว่าเป็น "รวมแล้ว" และเพิ่มสถานะให้
  • ฉันจะใช้เวลาอีกสองสามชั่วโมงในการขัดเกลา UI ฉันรู้ว่าฉันไม่เก่งในเรื่องการออกแบบภาพ และสิ่งที่ฉันสร้างก็มีความสวยงามไม่สวยงามนัก

แค่นั้นแหละ.

นั่นคือทั้งหมดที่ใช้โดยสุจริต การผลักดันไปสู่การใช้งานจริงใช้เวลาประมาณครึ่งชั่วโมง ดังนั้นฉันจึงสามารถอ่านคำแนะนำ การโหวต และความคิดเห็นได้ประมาณสิบกว่ารายการ และเพิ่มลงในฐานข้อมูลด้วยการอ้างอิงผู้ใช้ที่ถูกต้อง

เมื่อทุกอย่างพูดและทำเสร็จแล้ว ทุกอย่างก็โอเวอร์คล็อกไว้ที่โค้ดประมาณ 550 บรรทัด ไม่เลวเลยสำหรับการประหยัดเงิน $50/เดือน

บางสิ่งที่ฉันไม่ได้นำมาจากแคนนี่:

  • ความสามารถในการอัปโหลดภาพสำหรับโพสต์และความคิดเห็น
  • ความสามารถสำหรับผู้ใช้ในการแก้ไขโพสต์
  • ความสามารถในการลบ/แก้ไขความคิดเห็น
  • การค้นหาและจัดเรียงโพสต์ใหม่
  • ความสามารถในการดูว่าใครลงคะแนนให้กับไอเดียอื่นบ้าง จริงอยู่ที่ว่ามันไร้ประโยชน์เพราะชื่อทั้งหมดเป็นชื่อปลอมที่โง่เขลา
  • คุณสมบัติรองอื่นๆ ที่ผู้ใช้ต้องเผชิญซึ่งไม่คุ้มค่าที่จะสังเกต

ฉันไม่ได้นอนไม่หลับกับสิ่งเหล่านั้นเลย

หากคุณชอบโพสต์นี้และต้องการดู "Pinecast" คุณสามารถสมัครได้โดยไม่ต้องใช้บัตรเครดิตเพื่อทดลองใช้ได้นานเท่าที่คุณต้องการ หากคุณตัดสินใจว่าแผนแบบชำระเงินเหมาะกับคุณ คุณสามารถใช้โค้ด uncanny เพื่อรับส่วนลด 50% สำหรับบริการสองเดือนแรกสำหรับแผนใดก็ได้จนถึงสิ้นเดือนพฤษภาคม