Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
language: python
python:
- "2.7"
services:
- sqlite3
install:
- pip install -q Django==1.11.15
before_script:
- python manage.py migrate
script:
- python manage.py test list_posts

59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,57 @@
# ListPostsByRating
App developed in Python, using the Django library
[![Build Status](https://travis-ci.com/Ivopires/ListPostsByRating.svg?branch=challenge)](https://travis-ci.com/Ivopires/ListPostsByRating)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kudos for the documentation!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Miguel! Thanks for the feedback on the challenge

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gif


# List Posts By Rating
App developed in Python(2.7), using the Django(1.11.15) framework.

## How to run

### Setup Django (version 1.11.15):
1. Install pip (if you have not installed already): [Guide](https://packaging.python.org/tutorials/installing-packages/)
2. Install Django (version 1.11.15):
```
pip install Django==1.11.15
```

### Load db scheme
```
python manage.py migrate
```

### Populate db with Posts

```
python manage.py shell

>>>from list_posts.models import Posts
>>>post = Post(post_text='Example text')
>>>post.save()
```

### Run tests

```
python manage.py test list_posts
```

### Run the project
To run project you must follow the following steps:

1. Start the server:

```
python manage.py runserver
```

2. Go to the following link - [127.0.0.1:8000](http://127.0.0.1:8000)

There are four available endpoints:
```
/
/upvote/:post_id
/downvote/:post_id
/posts/
```

The first endpoint is responsible to list the latest 10 posts, ordered by posting date, if by accident 'future' posts are added to the database, these will not show on the page. The second and third endpoints are responsible to give the up/down votes, respectively, to the post with the <post_id>. Finally, the last endpoint, will show all the available posts, ordered by 'score', being the top-scored posts on the top and the less-scored posts on the bottom.

To achieve a post score that would allow to order different posts with the same up/down votes ratio, as mentioned on the challenge's README, a Wilson-score Interval metric was used, which value depends solely on the number of up/down votes, total number of votes of the post and on the __z__ value used by the normal distribution (which depends on the degree of confidence used).
Empty file added challenge/__init__.py
Empty file.
119 changes: 119 additions & 0 deletions challenge/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Django settings for challenge project.

Generated by 'django-admin startproject' using Django 1.11.15.

For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'hj&%tr8llp&+n*nrq_c!8p7$#f=ocd3w86iq=eh88@kv0-^1=r'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

# Application definition

INSTALLED_APPS = [
'list_posts.apps.ListPostsConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'challenge.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'challenge.wsgi.application'

# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}

# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME':
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME':
'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME':
'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME':
'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]

# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'
22 changes: 22 additions & 0 deletions challenge/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""challenge URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
url(r'^', include('list_posts.urls')),
url(r'^admin/', admin.site.urls),
]
16 changes: 16 additions & 0 deletions challenge/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for challenge project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "challenge.settings")

application = get_wsgi_application()
Empty file added list_posts/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions list_posts/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.contrib import admin

# Register your models here.
8 changes: 8 additions & 0 deletions list_posts/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.apps import AppConfig


class ListPostsConfig(AppConfig):
name = 'list_posts'
25 changes: 25 additions & 0 deletions list_posts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-17 22:48
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Post',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('post_text', models.CharField(max_length=500, verbose_name='Post content')),
('up_votes', models.PositiveIntegerField(default=0, verbose_name='Up votes')),
('down_votes', models.PositiveIntegerField(default=0, verbose_name='Down votes')),
],
),
]
22 changes: 22 additions & 0 deletions list_posts/migrations/0002_post_pub_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-18 14:05
from __future__ import unicode_literals

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('list_posts', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='post',
name='pub_date',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date Published'),
preserve_default=False,
),
]
20 changes: 20 additions & 0 deletions list_posts/migrations/0003_post_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-18 19:51
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('list_posts', '0002_post_pub_date'),
]

operations = [
migrations.AddField(
model_name='post',
name='score',
field=models.FloatField(default=0, verbose_name='Post Score'),
),
]
Empty file.
45 changes: 45 additions & 0 deletions list_posts/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import numpy as np

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you feel the need to use numpy here?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to used the numpy library in order to create more precise and consistent floats with 64-bit representation, which were used in the compute_score method implemented.

from django.utils import timezone
from math import sqrt
from django.db import models
from django.utils.encoding import python_2_unicode_compatible


@python_2_unicode_compatible
class Post(models.Model):
pub_date = models.DateTimeField('Date Published', default=timezone.now)
post_text = models.CharField('Post content', max_length=500)
up_votes = models.PositiveIntegerField('Up votes', default=0)
down_votes = models.PositiveIntegerField('Down votes', default=0)
score = models.FloatField('Post Score', default=0)

def __str__(self):
return self.post_text

def compute_score(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the application started growing and adding a lot of new functionalities, would you keep the business logic here in the models?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, I think the best solution is to implement a new class with the business logic implemented here inside the model, and then move this method to that class. And from there one, implement different classes with different purposes.

"""
The compute_score function will compute the Wilson-score Interval,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the advantages of using this score algorithm here, as opposed to a simpler one?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reasoning behind my decision was to solve the problem that was specified in the challenge description, where 2 posts with the same ratio (ex: 60 up and 40 down votes, and 600 up and 400 down) would have to be evaluated with a different score rather than a simple ratio between up/down votes (which could be a possible implementation of a simpler algorithm)

which calculation depends on the number of up/down votes and the total
number of votes.
Where:
- p_hat - is the fraction of up votes out of total votes
- total_votes - is the total number of votes
- z - is the normal distribution, which, in order to have 95%
of confidence, has to be equal to 1.96
"""

total_votes = self.up_votes + self.down_votes
if self.up_votes == 0:
return 0
else:
p_hat = np.float64(self.up_votes) / total_votes
z = np.float64(1.96)

lower_bound = (((p_hat + z * z / (2 * total_votes)) - z * sqrt(
(p_hat * (1 - p_hat) + z * z /
(4 * total_votes)) / total_votes)) / (1 + z * z / total_votes))

return lower_bound
3 changes: 3 additions & 0 deletions list_posts/static/list_posts/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
15 changes: 15 additions & 0 deletions list_posts/templates/list_posts/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% load static %}

<link rel="stylesheet" type="text/css" href="{% static 'list_posts/style.css' %}" />

{% if latest_posts_list %}
<ul style="list-style: none;">
{% for post in latest_posts_list %}
<li style="font-family: Arial, Helvetica, sans-serif;">
<a href="{% url 'list_posts:post' post.id %}">{{post.post_text}}</a>, published on {{post.pub_date}}
</li><br />
{% endfor %}
</ul>
{% else %}
<p>No posts are available.</p>
{% endif %}
Loading