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
6 changes: 1 addition & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ os:
python:
- "3.4"
- "3.5"
- "2.7"

install:
- "pip install requests"
- "pip install -r requirements.txt"
- "pip install selenium"
- "pip install prospector"

before_script:
- "python main.py &"
Expand All @@ -20,6 +18,4 @@ before_script:
- "sleep 1"

script:
- "prospector --strictness veryhigh"
- "python tests.py"
- "python functional_tests.py"
674 changes: 0 additions & 674 deletions LICENSE

This file was deleted.

37 changes: 0 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,11 @@
# ДЗ: первый мини-проект [![Build Status](https://travis-ci.org/pahaz/homework-simple-python-web-application.svg?branch=master)](https://travis-ci.org/pahaz/homework-simple-python-web-application) #

Предлагается сделать один из следующих мини-проектов:

1. [сервис "плохой-сайт.ру"](#1-%D0%BF%D0%BB%D0%BE%D1%85%D0%BE%D0%B9-%D1%81%D0%B0%D0%B9%D1%82%D1%80%D1%83)
2. [сервис "слил-файл.ру"](#2-%D1%81%D0%BB%D0%B8%D0%BB-%D1%84%D0%B0%D0%B9%D0%BB%D1%80%D1%83)
3. [сервис "сократи-ссылку.ру"](#3-%D1%81%D0%BE%D0%BA%D1%80%D0%B0%D1%82%D0%B8-%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D1%83%D1%80%D1%83)
4. [сервис "расшарь-код.ру"](#4-%D1%80%D0%B0%D1%81%D1%88%D0%B0%D1%80%D1%8C-%D0%BA%D0%BE%D0%B4%D1%80%D1%83)

### 1. "плохой-сайт.ру" ###
**книга жалоб интернета** или **www.huzhe.net**
- возможность оставить жалобу на какой-то ресурс
- возможность редактирования своей жалобы
- возможность удаления свой жалобы

### 2. "слил-файл.ру" ###
**файлообменник для студентов** или **ge.tt** или **anonfiles.com**
- возможность заливать/скачивать файлы
- возможность посмотреть количество скачиваний
- возможность удалить свой файл
- режим "удалить файл после первого скачивания"

### 3. "сократи-ссылку.ру" ###
**твиттер символо-экономилка** или **bitly.com**
- возможность сокращения ссылки
- возможность посмотреть количество переходов по ссылке
- возможность удалить свою ссылку
- режим "удалить ссылку после первого перехода"

### 4. "расшарь-код.ру" ###
**кода-копипаста** или **pastebin.com**
- возможность вставки/просмотра кода с сохранением форматирования
- подсветка синтаксиса (http://pygments.org/)
- возможность удалить свой код
- режим "удалить код после первого просмотра"

### Интересные идеи микро-проектов: ###
- online base64, base32, base16, MD5, AES, SHA-1, ...
- online unzip, unrar, untar, ungz, ...
- online text diff
- online encoding detector (ru)
- online python, ruby, node.js, php, ...
- online compile, C#, C++, ANSI C, ...
- online port scan

# Требования #

Для выполнения задания можно использовать любой из рассмотренных фреймворков:
Expand Down
96 changes: 91 additions & 5 deletions functional_tests.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,95 @@
import unittest
import random
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

browser = webdriver.Firefox()
browser.get('http://localhost:8000')
page_source = browser.page_source

assert 'mini' in page_source

browser.quit()

class Shoter(unittest.TestCase):

def _create_shot_link(self, driver, link='https://www.google.ru/', one_off=False, password=''):
driver.get(self.address)
full_link = driver.find_element_by_name('full_link')
full_link.send_keys(link)
if password:
passw = driver.find_element_by_name('password')
passw.send_keys(password)
if one_off:
one_off = driver.find_element_by_name('one_off')
one_off.click()
submit = driver.find_element_by_name('submit')
submit.submit()
shot_link = driver.find_element_by_name('new_link').get_attribute('value')
edit_link = driver.find_element_by_name('adm_link').get_attribute('value')
return shot_link, edit_link

def setUp(self):
self.address = 'http://127.0.0.1:5000/'
self.driver = webdriver.Firefox()

def test_index_page(self):
driver = self.driver
driver.get(self.address)
self.assertIn("Shoter", driver.title)

def test_404(self):
driver = self.driver
driver.get(self.address + 'wehavenothispageonsite')
self.assertEqual(self.address, driver.current_url)

def test_redirect_by_link(self):
driver = self.driver
shot_link, adm_link = self._create_shot_link(driver)
driver.get(shot_link)
self.assertEqual('https://www.google.ru/', driver.current_url)

def test_one_off_redirect(self):
driver = self.driver
shot_link, adm_link = self._create_shot_link(driver, one_off=True)
driver.get(shot_link)
driver.get(shot_link)
self.assertEqual(self.address, driver.current_url)

def test_number_of_redirect(self):
driver = self.driver
shot_link, adm_link = self._create_shot_link(driver)
num = random.randint(1, 5)
for x in range(num):
driver.get(shot_link)
driver.get(adm_link)
self.assertIn('Количество переходов по ссылке: {0}'.format(num), driver.page_source)

def test_fake_password(self):
driver = self.driver
shot_link, adm_link = self._create_shot_link(driver, password='a')
driver.get(adm_link)
password = driver.find_element_by_name('password')
password.send_keys('b')
password.submit()
self.assertIn('Пароль неверен', driver.page_source)

def test_true_password(self):
driver = self.driver
shot_link, adm_link = self._create_shot_link(driver, password='a')
driver.get(adm_link)
password = driver.find_element_by_name('password')
password.send_keys('a')
password.submit()
self.assertIn('URL успешно удален', driver.page_source)

def test_remove_url(self):
driver = self.driver
shot_link, adm_link = self._create_shot_link(driver, password='a')
driver.get(adm_link)
password = driver.find_element_by_name('password')
password.send_keys('a')
password.submit()
driver.get(shot_link)
self.assertEqual(self.address, driver.current_url)

def tearDown(self):
self.driver.close()

if __name__ == "__main__":
unittest.main()
196 changes: 179 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,190 @@
from wsgiref.simple_server import make_server
#!/usr/bin/env python3
import hashlib
import random
import string

__author__ = 'pahaz'
from flask import Flask, render_template, redirect, url_for, flash, abort
from flask.ext.bootstrap import Bootstrap
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.wtf import Form
from wtforms import BooleanField, StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, URL, Length

__author__ = 'Igor Trofimov'

def application(environ, start_response):
assert environ.get('PATH_INFO') is not None, "environ['PATH_INFO'] is None"
app = Flask(__name__)

status = "200 OK"
headers = [('Content-type', 'text/html; charset=utf-8')]
body = """<!DOCTYPE html>
<h1>Example-mini-application</h1>
# Настройки приложения
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SECRET_KEY'] = 'SECRET_KEY'

db = SQLAlchemy(app)
Bootstrap(app)


class LinkModel(db.Model):
"""
Модель ссылки в бд
"""
full_link = db.Column(db.String(2000))
one_off = db.Column(db.Boolean)
shot_link = db.Column(db.String(10), primary_key=True, unique=True, index=True)
password_hash = db.Column(db.String(32))
number_of_visits = db.Column(db.Integer, default=0)

def __init__(self, full_link: 'Строка исходного полного URL', one_off: 'Флаг одноразового использования' = False,
password: 'Строка пароля для удаления' = app.config['SECRET_KEY']) -> None:
"""
Конструктор класса LinkModel(db.Model)
"""
self.full_link = full_link
self.one_off = one_off
self.password_hash = hashlib.md5(password.encode()).hexdigest()
self._set_shot_link()

def _set_shot_link(self) -> None:
"""
Генерирует и устанавливает короткий локальный URL
"""
name = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(10))
while LinkModel.query.filter_by(shot_link=name).first():
name = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(10))
self.shot_link = name

def verify_password(self, password: 'Строка пароля для удаления') -> 'Флаг проверки пароля':
"""
Проверяет введенный пароль на соответствие установленному
"""
if hashlib.md5(password.encode()).hexdigest() == self.password_hash:
return True
return False

def visit(self) -> None:
"""
Считает количество переходов по сокращенному URL
"""
self.number_of_visits += 1

def __repr__(self) -> 'Строка описания объекта':
"""
Генерирует строку описания объекта
"""
return '<Link {0}>'.format(self.link)


class AddLinkForm(Form):
"""
Модель формы добавления нового URL
"""
full_link = StringField('URL, который следует сократить',
validators=[
DataRequired('URL не должен быть пустым'),
Length(5, 2000, message='Длина URL должна быть от 5 до 2000 символов'),
URL(require_tld=True, message='Некорректный формат URL')
])
password = PasswordField('Пароль для удаления')
one_off = BooleanField('Автоматически удалить после 1 захода')
submit = SubmitField('Сократить')


class ShowLinkForm(Form):
"""
Модель формы отображения короткого URL
"""
new_link = StringField('Новый короткий URL')
adm_link = StringField('URL для администратирования')
submit = SubmitField('Сократить еще одну')


class DeleteLinkForm(Form):
"""
Модель формы удаления короткого URL
"""
password = PasswordField('Пароль для удаления')
submit = SubmitField('Удалить')


@app.route('/', methods=['GET', 'POST'])
def index() -> 'Подготовленный ответ сервера':
"""
Генерирует главную страницу и обрабатывает форму добавления нового URL
"""
add_link_form = AddLinkForm()
# Если форма добавления нового URL заполнена верно
if add_link_form.validate_on_submit():
link = LinkModel(
full_link=add_link_form.full_link.data,
one_off=add_link_form.one_off.data,
password=add_link_form.password.data
)
db.session.add(link)
db.session.commit()
return redirect(url_for('show', shot_link=link.shot_link))
return render_template('SimpleForm.html', link_form=add_link_form)

start_response(status, headers)
return [body.encode('utf-8')]

@app.route('/show/<shot_link>', methods=['GET', 'POST'])
def show(shot_link: 'Строка короткого локального URL') -> 'Подготовленный ответ сервера':
"""
Генерирует страницу отображения информации о коротком URL
"""
show_link_form = ShowLinkForm()
# Если нажали кнопку перехода на главную
if show_link_form.validate_on_submit():
return redirect(url_for('index'))
show_link_form.new_link.data = url_for('red', shot_link=shot_link, _external=True)
show_link_form.adm_link.data = url_for('edit', shot_link=shot_link, _external=True)
return render_template('SimpleForm.html', link_form=show_link_form)

def run(host='', port=31338):
print("It's work! Visit http://{host}:{port}/".format(
host=host or 'localhost',
port=port))
httpd = make_server(host, port, application)
httpd.serve_forever()

@app.route('/edit/<shot_link>', methods=['GET', 'POST'])
def edit(shot_link: 'Строка короткого локального URL') -> 'Подготовленный ответ сервера':
"""
Генерирует страницу редактирования и обрабатывает форму удаления короткого URL
"""
delete_link_form = DeleteLinkForm()
link = LinkModel.query.filter_by(shot_link=shot_link).first()
if link:
flash('Количество переходов по ссылке: {0}'.format(link.number_of_visits))
# Если форма заполнена верно
if delete_link_form.validate_on_submit():
# Если пароль верен
if link.verify_password(delete_link_form.password.data):
db.session.delete(link)
db.session.commit()
flash('URL успешно удален')
return redirect(url_for('index'))
flash('Пароль неверен')
return render_template('SimpleForm.html', link_form=delete_link_form)
return abort(404)


@app.route('/<shot_link>')
def red(shot_link: 'Строка короткого локального URL') -> 'Подготовленный ответ сервера':
"""
Выполняет обработку запроса сокращенной ссылки
"""
link = LinkModel.query.filter_by(shot_link=shot_link).first()
if link:
link.visit()
url = link.full_link
if link.one_off:
db.session.delete(link)
db.session.commit()
return redirect(url, code=303)
return abort(404)


@app.errorhandler(404)
def page_not_found(e: 'Объект ошибки 404') -> 'Подготовленный ответ сервера':
"""
Обработка ошибки 404
"""
flash('Такой страницы не существует. Вы были перенаправлены на главную страницу')
return redirect(url_for('index'))


db.create_all()
if __name__ == "__main__":
run()
app.run(debug=False)
12 changes: 12 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
dominate==2.2.0
Flask==0.10.1
Flask-Bootstrap==3.3.5.7
Flask-SQLAlchemy==2.1
Flask-WTF==0.12
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
SQLAlchemy==1.0.12
visitor==0.1.2
Werkzeug==0.11.8
WTForms==2.1
Loading