-
Notifications
You must be signed in to change notification settings - Fork 0
List Posts by Rating #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9868c68
92165d9
38c8f48
a39af4e
cabd661
a5d9bca
c62ce65
d58de16
66c7664
103b591
59c233c
b0fce1f
11f4d55
a8348b1
6644dcf
b2cbfb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,57 @@ | ||
| # ListPostsByRating | ||
| App developed in Python, using the Django library | ||
| [](https://travis-ci.com/Ivopires/ListPostsByRating) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| # 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). | ||
| 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/' |
| 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), | ||
| ] |
| 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() |
| 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. |
| 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' |
| 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')), | ||
| ], | ||
| ), | ||
| ] |
| 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, | ||
| ), | ||
| ] |
| 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'), | ||
| ), | ||
| ] |
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you feel the need to use numpy here?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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; | ||
| } |
| 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 %} |

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kudos for the documentation!
There was a problem hiding this comment.
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