diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0a113f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.DS_Store \ No newline at end of file diff --git a/source/Dockerfile b/source/Dockerfile new file mode 100644 index 0000000..6bccc09 --- /dev/null +++ b/source/Dockerfile @@ -0,0 +1,7 @@ + FROM python:2.7 + ENV PYTHONUNBUFFERED 1 + RUN mkdir /code + WORKDIR /code + ADD requirements.txt /code/ + RUN pip install -r requirements.txt + ADD . /code/ \ No newline at end of file diff --git a/source/README.md b/source/README.md new file mode 100644 index 0000000..59a404b --- /dev/null +++ b/source/README.md @@ -0,0 +1,6 @@ +## What needs to be done to run the project: + +1. docker-compose build +2. docker-compose run web ./manage.py migrate +3. docker-compose run web ./manage.py createsuperuser +4. docker-compose up diff --git a/source/commits/__init__.py b/source/commits/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/commits/admin.py b/source/commits/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/source/commits/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/source/commits/apps.py b/source/commits/apps.py new file mode 100644 index 0000000..6076d3b --- /dev/null +++ b/source/commits/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class ApigithubConfig(AppConfig): + name = 'apigithub' diff --git a/source/commits/migrations/0001_initial.py b/source/commits/migrations/0001_initial.py new file mode 100644 index 0000000..ad07e04 --- /dev/null +++ b/source/commits/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-03-15 09:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Commits', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sha', models.CharField(default='', max_length=200)), + ('author', models.CharField(blank=True, default='', max_length=200)), + ('pub_date', models.DateTimeField()), + ('text', models.TextField()), + ('read_status', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'commits', + }, + ), + ] diff --git a/source/commits/migrations/__init__.py b/source/commits/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/commits/models.py b/source/commits/models.py new file mode 100644 index 0000000..e624f69 --- /dev/null +++ b/source/commits/models.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. +class Commits(models.Model): + class Meta(): + db_table = 'commits' + + + sha = models.CharField(max_length=200, default='') + author = models.CharField(max_length=200, blank=True, default='') + pub_date = models.DateTimeField() + text = models.TextField() + read_status = models.BooleanField(default=False) + diff --git a/source/commits/tasks.py b/source/commits/tasks.py new file mode 100644 index 0000000..1327838 --- /dev/null +++ b/source/commits/tasks.py @@ -0,0 +1,37 @@ +from celery.task.schedules import crontab +from celery.decorators import periodic_task +from celery.utils.log import get_task_logger + +from django.utils import timezone + +from .models import Commits +from urllib2 import urlopen +from dateutil.parser import parse + +import json + +logger = get_task_logger(__name__) + + +@periodic_task(ignore_result=True, run_every=(crontab(hour="*", minute=timezone.now().minute+1, day_of_week="*"))) +def get_latest_commit(owner='nodejs', repo='node'): + logger.info("Start task") + url = 'https://api.github.com/repos/{owner}/{repo}/commits'.format(owner=owner, repo=repo) + response = urlopen(url).read() + data = json.loads(response.decode('UTF-8')) + list = data[:25] + for el in list: + sha = el['sha'] + name = el['commit']['author']['name'] + msg = el['commit']['message'].encode('ascii','ignore') + date = parse(el['commit']['author']['date']) + try: + if not Commits.objects.filter(sha=sha): + comment = Commits(author=name, text=msg, pub_date=date, read_status=False, sha=sha) + comment.save() + logger.info("Get!!!") + except Exception as ex: + print('ex is: {}'.format(ex)) + logger.info("End task success!!!") + return list + \ No newline at end of file diff --git a/source/commits/templates/commits.html b/source/commits/templates/commits.html new file mode 100644 index 0000000..bed034d --- /dev/null +++ b/source/commits/templates/commits.html @@ -0,0 +1,34 @@ +{% extends 'index.html' %} + +{% block commits %} +
+

Commits!

+ {% for commit in commits %} +

{{ commit.author }} - {{ commit.pub_date }}

+

{{ commit.text }}

+ Read - {{ commit.read_status }} +
+ {% endfor %} +
+
+ {% if commits.has_previous %} + < + {% else %} + < + {% endif %} + {% for page in commits.paginator.page_range %} + {% if page == commits.number %} + {{ page }} + {% else %} + {{ page }} + {% endif %} + {% endfor %} + {% if commits.has_next %} + > + {% else %} + > + {% endif %} + +
+{% endblock %} + \ No newline at end of file diff --git a/source/commits/tests.py b/source/commits/tests.py new file mode 100644 index 0000000..5440075 --- /dev/null +++ b/source/commits/tests.py @@ -0,0 +1,25 @@ +from urllib2 import urlopen +import json + +from django.test import TestCase + +from .tasks import get_latest_commit +from .models import Commits + + +class TasksMethodTests(TestCase): + + def test_get_latest_commit(self): + """ + test_get_latest_commit - checking get_latest_commit saved only new commits + """ + error = '' + list = get_latest_commit() + for el in list: + sha = el['sha'] + try: + if not Commits.objects.get(sha=sha): + error = 'object not exist' + except Exception as ex: + error = 'there are duplicates' + self.assertEqual(error, '') \ No newline at end of file diff --git a/source/commits/urls.py b/source/commits/urls.py new file mode 100644 index 0000000..d394397 --- /dev/null +++ b/source/commits/urls.py @@ -0,0 +1,24 @@ +"""composeexample URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/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 +from views import commits, readed + + +urlpatterns = [ + url(r'^page/(\d+)/$', commits), + url(r'^readed/(?P\d+)/(?P\d+)/$', readed), + url(r'^$', commits), +] diff --git a/source/commits/views.py b/source/commits/views.py new file mode 100644 index 0000000..9ac38ac --- /dev/null +++ b/source/commits/views.py @@ -0,0 +1,30 @@ +from django.shortcuts import render_to_response, redirect +from django.http.response import Http404 +from django.core.paginator import Paginator +from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone + +from .models import Commits + +# Create your views here. + +def commits(request, page_number=1): + + all_commits = Commits.objects.order_by('-pub_date') + current_page = Paginator(all_commits, 5) + print(timezone.now().minute) + return render_to_response('commits.html', {'commits': current_page.page(page_number)}) + +def readed(request, commit_id, page_number): + try: + commits = Commits.objects.get(id=commit_id) + if commits.read_status: + commits.read_status = False + else: + commits.read_status = True + commits.save() + response = redirect('/page/'+page_number+"/") + return response + except ObjectDoesNotExist: + raise Http404 + return redirect('/page/'+page_number+'/') diff --git a/source/db.sqlite3 b/source/db.sqlite3 new file mode 100644 index 0000000..13fb7cc Binary files /dev/null and b/source/db.sqlite3 differ diff --git a/source/docker-compose.yml b/source/docker-compose.yml new file mode 100644 index 0000000..8ac732b --- /dev/null +++ b/source/docker-compose.yml @@ -0,0 +1,31 @@ + version: '2' + services: + db: + image: postgres + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/code + ports: + - "8000:8000" + depends_on: + - db + # RabbitMQ + rabbit: + hostname: rabbit + image: rabbitmq:3.6.0 + environment: + - RABBITMQ_DEFAULT_USER=admin + - RABBITMQ_DEFAULT_PASS=mypass + ports: + - "5672:5672" # we forward this port because it's useful for debugging + - "15672:15672" # here, we can access rabbitmq management plugin + celery: + build: . + environment: + - C_FORCE_ROOT=true + command: python manage.py celeryd -l INFO -B + volumes: + - .:/code + diff --git a/source/github/__init__.py b/source/github/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/github/settings.py b/source/github/settings.py new file mode 100644 index 0000000..f3844cd --- /dev/null +++ b/source/github/settings.py @@ -0,0 +1,156 @@ +""" +Django settings for github project. + +Generated by 'django-admin startproject' using Django 1.10.6. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" + +import os +import djcelery + +djcelery.setup_loader() + +# 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.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '#d&h$dv!$zm_of%e9abqc(ib_(gie8_b_(!8eokb3y@*e+#m*%' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +CELERYBEAT_SCHEDULER = "djcelery.schedulers.DatabaseScheduler" +CELERY_ACCEPT_CONTENT = ['pickle', 'json', 'msgpack', 'yaml'] +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'commits', + "djcelery", +] +RABBIT_HOSTNAME = os.environ.get('RABBIT_PORT_5672_TCP', 'rabbit') + +if RABBIT_HOSTNAME.startswith('tcp://'): + RABBIT_HOSTNAME = RABBIT_HOSTNAME.split('//')[1] + +BROKER_URL = os.environ.get('BROKER_URL', + '') +if not BROKER_URL: + BROKER_URL = 'amqp://{user}:{password}@{hostname}/{vhost}/'.format( + user=os.environ.get('RABBIT_ENV_USER', 'admin'), + password=os.environ.get('RABBIT_ENV_RABBITMQ_PASS', 'mypass'), + hostname=RABBIT_HOSTNAME, + vhost=os.environ.get('RABBIT_ENV_VHOST', '')) + +# We don't want to have dead connections stored on rabbitmq, so we have to negotiate using heartbeats +BROKER_HEARTBEAT = '?heartbeat=30' +if not BROKER_URL.endswith(BROKER_HEARTBEAT): + BROKER_URL += BROKER_HEARTBEAT + +BROKER_POOL_LIMIT = 1 +BROKER_CONNECTION_TIMEOUT = 10 + +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 = 'github.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates'), + os.path.join(BASE_DIR, 'commits/templates'), + ], + '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 = 'github.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + # 'default': { + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + # } + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'postgres', + 'HOST': 'db', + 'PORT': 5432, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.10/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.10/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.10/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/source/github/urls.py b/source/github/urls.py new file mode 100644 index 0000000..2a82b9d --- /dev/null +++ b/source/github/urls.py @@ -0,0 +1,23 @@ +"""composeexample URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/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'^admin/', admin.site.urls), + url(r'^', include('commits.urls')), +] diff --git a/source/github/wsgi.py b/source/github/wsgi.py new file mode 100644 index 0000000..e7d3874 --- /dev/null +++ b/source/github/wsgi.py @@ -0,0 +1,17 @@ +""" +WSGI config for composeexample 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.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "composeexample.settings") +os.environ["CELERY_LOADER"] = "django" + +application = get_wsgi_application() diff --git a/source/manage.py b/source/manage.py new file mode 100755 index 0000000..e838fd4 --- /dev/null +++ b/source/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "github.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/source/requirements.txt b/source/requirements.txt new file mode 100644 index 0000000..d079016 --- /dev/null +++ b/source/requirements.txt @@ -0,0 +1,6 @@ + Django + psycopg2 + python-dateutil + django-celery + python-dateutil + \ No newline at end of file diff --git a/source/templates/index.html b/source/templates/index.html new file mode 100644 index 0000000..dc4653a --- /dev/null +++ b/source/templates/index.html @@ -0,0 +1,9 @@ + + + + Django + + + {% block commits %}{% endblock %} + + \ No newline at end of file