-

parents
import math
import timeit
def rectangle_method(f, a, b, n, mode='left'):
h = (b - a) / n
points = [a + i * h for i in range(n + 1)]
if mode == 'left':
values = [f(points[i]) for i in range(n)]
else:
values = [f(points[i + 1]) for i in range(n)]
return h * sum(values)
# метод трапеций
# а и b - диапазон интегрирования
# f - функция
# n_iter - число разбиений
def integrate(f, a, b, n):
h = (b - a) / n # Шаг интегрирования
result = 0.5 * (f(a) + f(b))
for i in range(1, n):
x_i = a + i * h
result += f(x_i)
result *= h
return result
if __name__ == '__main__':
print(integrate(math.sin, 0, 1, n=100))
print(rectangle_method(math.sin, 0, 1, 100, mode='left'))
# time = timeit.timeit("integrate(sin, 0, pi, n_iter=1000)",
# setup="from math import sin, pi", number=100) * 1000 # msec
# print(f"Время выполнения: {time} мс")
Выполнила Таринская Татьяна
import pytest
from sem_7.lab_1.main import integrate
from math import sin, pi
@pytest.mark.parametrize("n_iter", [10**4, 10**5, 10**6])
def test_integration_sin(n_iter):
a = 0
b = pi
result = integrate(sin, a, b, n_iter)
expected_result = 2.0 # Интеграл sin(x) от 0 до pi равен 2.0
assert abs(result - expected_result) < 1e-6
\ No newline at end of file
import math
import timeit
import threading
def integrate(f, a, b, n):
h = (b - a) / n
result = 0.5 * (f(a) + f(b))
for i in range(1, n):
x_i = a + i * h
result += f(x_i)
result *= h
return result
class Integrator:
def __init__(self):
self.lock = threading.Lock()
self.result = 0.0
def integrate_subinterval(self, f, a, b, n):
self.lock.acquire()
try:
partial_result = integrate(f, a, b, n)
self.result += partial_result
finally:
self.lock.release()
def main():
a = 0
b = 1
n = 1000
num_threads = 4
threads = []
integrator = Integrator()
for i in range(num_threads):
start = a + i * (b - a) / num_threads
end = a + (i + 1) * (b - a) / num_threads
thread = threading.Thread(target=integrator.integrate_subinterval,
args=(math.sin, start, end, n // num_threads))
threads.append(thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
final_result = integrator.result
print("Интеграл:", final_result)
if __name__ == "__main__":
execution_time = timeit.timeit(main, number=1)
print("Время выполнения:", execution_time, "секунд")
import threading
#Напишите программу, которая создаёт несколько потоков, а затем выводит их имена изнутри каждого потока.
def print_thread_name():
thread_name = threading.current_thread().name
print(f"Привет, я поток {thread_name}")
def main():
num_threads = 5
threads = []
for i in range(num_threads):
thread = threading.Thread(target=print_thread_name)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
if __name__ == "__main__":
main()
import requests
import threading
# Напишите программу для одновременной загрузки нескольких файлов (например картинок) с использованием потоков.
# Используйте для скачивания одну из библиотек urllib, requests или wget.
file_urls = [
"https://source.unsplash.com/user/c_v_r/1900x800",
"https://source.unsplash.com/user/c_v_r/100x100",
]
def download_file(url):
response = requests.get(url, stream=True)
if response.status_code == 200:
file_name = url.split("/")[-1]
with open(file_name, "wb") as file:
file.write(response.content)
print(f"Загружен файл: {file_name}")
else:
print(f"Не удалось загрузить файл: {url}")
def main():
threads = []
for url in file_urls:
thread = threading.Thread(target=download_file, args=(url,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
if __name__ == "__main__":
main()
import requests
import concurrent.futures
# Напишите программу на Python, которая выполняет одновременные HTTP запросы с использованием потоков.
# Используйте библиотеку requests.
def make_request(url):
response = requests.get(url)
return response.text
def main():
urls = [
"https://docs.python.org/3/",
"https://habr.com",
"https://tproger.ru"
]
results = []
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {executor.submit(make_request, url): url for url in urls}
for future in concurrent.futures.as_completed(futures):
url = futures[future]
try:
response = future.result()
print(f"Запрос к {url} завершен.")
results.append(response)
except Exception as e:
print(f"Запрос к {url} завершился с ошибкой: {str(e)}")
if __name__ == "__main__":
main()
import concurrent.futures
# Напишите программу для вычисления факториала числа с использованием нескольких потоков.
def factorial(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
def main():
n = int(input("Введите число для вычисления факториала: "))
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(factorial, n)
print("Вычисление факториала...")
result = future.result()
print(f"Факториал {n} равен {result}")
if __name__ == "__main__":
main()
# Напишите программу для реализации многопоточного алгоритма быстрой сортировки.
import threading
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
def threaded_quicksort(arr, depth=0):
if len(arr) <= 1:
return arr
if depth > 2:
return quicksort(arr)
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
left_thread = threading.Thread(target=threaded_quicksort, args=(left, depth + 1))
right_thread = threading.Thread(target=threaded_quicksort, args=(right, depth + 1))
left_thread.start()
right_thread.start()
left_thread.join()
right_thread.join()
return quicksort(left) + middle + quicksort(right)
if __name__ == "__main__":
my_list = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
sorted_list = threaded_quicksort(my_list)
print("Original List:", my_list)
print("Sorted List:", sorted_list)
# Дополните решение из лабораторной работы № 1 с кодом функции integrate.
# Проведите замеры времени вычисления для разного числа потоков и процессов.
import math
import concurrent.futures as ftres
from functools import partial
def integrate(f, a, b, *, n=1000):
h = (b - a) / n # Шаг интегрирования
result = 0.5 * (f(a) + f(b))
for i in range(1, n):
x_i = a + i * h
result += f(x_i)
result *= h
return result
def foo():
n_jobs = 4
a = 0
b = 1
f = lambda x: x
executor = ftres.ThreadPoolExecutor(max_workers=n_jobs)
# future_result = executor.submit(identity,10)
# ai, bi = 0, 0
step = (b - a) / n_jobs
fs = [(a + i * step, a + (i + 1) * step) for i in range(n_jobs)]
print(step, fs)
spawn_lst = []
for i in fs:
spawn = partial(executor.submit, integrate, f, i[0], i[1])
spawn_lst.append(spawn)
res = []
for f in spawn_lst:
res.append(f())
print(res)
s = [r.result() for r in ftres.as_completed(res)]
print(sum(s))
return res
\ No newline at end of file
# Напишите программу, которая будет симулировать банк с использованием потоков и объектов типа Lock. Реализуйте методы deposit и
# withdraw, которые будут добавлять и снимать деньги со счета клиента
# соответственно. Гарантируйте, что доступ к счету будет синхронизирован с помощью объекта типа Lock.
import threading
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
self.lock = threading.Lock()
def deposit(self, amount):
with self.lock:
current_balance = self.balance
new_balance = current_balance + amount
self.balance = new_balance
print(f"Deposited {amount}. New balance: {new_balance}")
def withdraw(self, amount):
with self.lock:
current_balance = self.balance
if amount <= current_balance:
new_balance = current_balance - amount
self.balance = new_balance
print(f"Withdrew {amount}. New balance: {new_balance}")
else:
print("Insufficient funds.")
def simulate_bank_operations(account):
for _ in range(5):
account.deposit(100)
account.withdraw(50)
if __name__ == "__main__":
bank_account = BankAccount()
threads = []
for _ in range(3):
thread = threading.Thread(target=simulate_bank_operations, args=(bank_account,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print("Final balance:", bank_account.balance)
# Напишите программу, используя объекты типа Future, чтобы асинхронно скачать несколько изображений с Интернета. Каждое изображение должно быть загружено в отдельный поток и сохранено на
# диск. Используйте семафор, чтобы ограничить количество одновременно выполняющихся потоков загрузки, чтобы избежать блокировки по IP от сервера или серверов.
import concurrent.futures
import requests
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import BoundedSemaphore
image_urls = [
"https://source.unsplash.com/user/c_v_r/1900x800",
"https://source.unsplash.com/user/c_v_r/100x100",
]
output_folder = 'downloaded_images'
os.makedirs(output_folder, exist_ok=True)
semaphore = BoundedSemaphore(value=2)
def download_image(url, filename):
response = requests.get(url, stream=True)
with open(filename, 'wb') as file:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
file.write(chunk)
def download_image_async(url, filename):
with semaphore:
download_image(url, filename)
if __name__ == "__main__":
with ThreadPoolExecutor(max_workers=len(image_urls)) as executor:
futures = {executor.submit(download_image_async, url, os.path.join(output_folder, f"image_{i}.jpg")): url for i, url in enumerate(image_urls)}
for future in as_completed(futures):
url = futures[future]
try:
future.result()
print(f"Downloaded: {url}")
except Exception as e:
print(f"Failed to download {url}. Error: {e}")
# Создайте два потока, один из которых будет записывать данные в
# файл, а другой ­ считывать данные с файла. Используйте объекты типа Future, чтобы синхронизировать потоки и избежать гонки потоков.
import concurrent.futures
data_to_write = "Hello, World!"
file_name = "example.txt"
def write_to_file(data, file_name):
with open(file_name, 'w') as file:
file.write(data)
def read_from_file(file_name):
with open(file_name, 'r') as file:
return file.read()
if __name__ == "__main__":
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
future_write = executor.submit(write_to_file, data_to_write, file_name)
future_read = executor.submit(read_from_file, file_name)
concurrent.futures.wait([future_write, future_read], return_when=concurrent.futures.ALL_COMPLETED)
result_write = future_write.result()
result_read = future_read.result()
print(f"Data written to file: {result_write}")
print(f"Data read from file: {result_read}")
# : Напишите программу, которая создает 3 потока. Первый поток должен устанавливать состояние объекта типа Event каждую секунду.
# Второй поток должен ждать наступления события и выводить сообщение "Event occurred". Третий поток должен выводить сообщение
# "Event did not occur" каждую секунду, до тех пор, пока не наступит событие. Как только событие происходит, третий поток должен
# остановиться.
import threading
import time
def event_setter(event):
while True:
time.sleep(1)
event.set()
def event_waiter(event):
print("Waiting for the event to occur...")
event.wait()
print("Event occurred")
def event_checker(event):
while not event.is_set():
print("Event did not occur")
time.sleep(1)
if __name__ == "__main__":
event = threading.Event()
setter_thread = threading.Thread(target=event_setter, args=(event,))
waiter_thread = threading.Thread(target=event_waiter, args=(event,))
checker_thread = threading.Thread(target=event_checker, args=(event,))
setter_thread.start()
waiter_thread.start()
checker_thread.start()
waiter_thread.join()
checker_thread.join()
print("All threads have finished.")
import threading
class ThreadSafeQueue:
def __init__(self):
self.queue = []
self.lock = threading.RLock()
def enqueue(self, item):
with self.lock:
self.queue.append(item)
def dequeue(self):
with self.lock:
if not self.is_empty():
return self.queue.pop(0)
else:
return None
def is_empty(self):
with self.lock:
return len(self.queue) == 0
def worker(queue, worker_id):
for i in range(5):
queue.enqueue(f"Task {i} from Worker {worker_id}")
print(f"Worker {worker_id} enqueued: Task {i}")
# Simulate some work
time.sleep(0.1)
item = queue.dequeue()
if item is not None:
print(f"Worker {worker_id} dequeued: {item}")
else:
print(f"Worker {worker_id} couldn't dequeue. Queue is empty.")
if __name__ == "__main__":
import time
thread_safe_queue = ThreadSafeQueue()
workers = []
for i in range(3):
worker_thread = threading.Thread(target=worker, args=(thread_safe_queue, i))
workers.append(worker_thread)
worker_thread.start()
for worker_thread in workers:
worker_thread.join()
print("All worker threads have finished.")
# Сделайте два потока, представляющие сервер и клиент, где клиентский поток будет ждать готовности сервера, прежде чем отправлять
# ему какой­либо запрос. Используйте threading.Barrier
import threading
import time
class ServerClientCommunication:
def __init__(self):
self.barrier = threading.Barrier(2) # Устанавливаем барьер для двух потоков
def server_function(self):
print("Server is waiting...")
self.barrier.wait() # Ожидаем, пока клиент не приготовится
print("Server received request")
def client_function(self):
print("Client is preparing...")
time.sleep(2) # Представим, что клиент выполняет какую-то подготовительную работу
print("Client is ready!")
self.barrier.wait() # Сообщаем серверу, что клиент готов
if __name__ == "__main__":
communication = ServerClientCommunication()
server_thread = threading.Thread(target=communication.server_function)
client_thread = threading.Thread(target=communication.client_function)
server_thread.start()
client_thread.start()
server_thread.join()
client_thread.join()
# Написать программу для параллельного поиска файла в директории.
# Каждый поток должен обрабатывать свой фрагмент файлов директории и синхронизироваться с другими потоками, чтобы убедиться, что
# файл найден только один раз. Как только первый файл по его шаблону
# найден все потоки поиска завершаются.
import os
import threading
class FileSearchThread(threading.Thread):
def __init__(self, directory, file_pattern):
super(FileSearchThread, self).__init__()
self.directory = directory
self.file_pattern = file_pattern
self.found_file = None
self.lock = threading.Lock()
def run(self):
for root, dirs, files in os.walk(self.directory):
for file in files:
if self.file_pattern in file:
with self.lock:
if self.found_file is None:
self.found_file = os.path.join(root, file)
print(f"File found by thread {threading.current_thread().name}: {self.found_file}")
return
if __name__ == "__main__":
target_directory = "/path/to/search" # Укажите путь к директории, в которой нужно искать файл
file_pattern = "target_file.txt" # Укажите имя или шаблон файла, который нужно найти
threads = []
for i in range(5): # Выберите количество потоков по вашему усмотрению
thread = FileSearchThread(target_directory, file_pattern)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
found_files = [thread.found_file for thread in threads if thread.found_file is not None]
if found_files:
print("All threads completed. First file found:", found_files[0])
else:
print("File not found.")
Выполнила Таринская Татьяна
## Комплект 1: Потоки в Python.
### 1.1
Напишите программу, которая создаёт несколько потоков, а затем выводит их имена изнутри каждого потока.
### 1.2
Напишите программу для одновременной загрузки нескольких файлов (например картинок) с использованием потоков. Используйте для скачивания одну из библиотек urllib, requests или wget.
### 1.3
Напишите программу на Python, которая выполняет одновременные HTTP­запросы с использованием потоков. Используйте библиотеку requests.
### 1.4
Напишите программу для вычисления факториала числа с использованием нескольких потоков.
### 1.5
Напишите программу для реализации многопоточного алгоритма быстрой сортировки.
## Комплект 2: Concurrency и Futures в Python.
### 2.1
Дополните решение из лабораторной работы № 1 с кодом функции integrate. Проведите замеры времени вычисления для разного числа потоков и процессов.
### 2.2
Напишите программу, симулирующую банк с использованием потоков и объектов типа Lock.
### 2.3
Напишите программу с использованием объектов типа Future для асинхронной загрузки изображений из Интернета.
### 2.4
Создайте два потока для записи и чтения данных из файла с использованием объектов типа Future.
### 2.5
Напишите программу с тремя потоками, один из которых устанавливает состояние объекта типа Event, второй ждет наступления события, а третий выводит сообщения в зависимости от наступления события.
### 2.6
Создайте программу с классом "очередь" (Queue), использующим рекурсивный блокировщик RLock.
### 2.7
Создайте программу с серверным и клиентским потоками, использующими threading.Barrier.
### 2.8
Написать программу для параллельного поиска файла в директории, синхронизируя потоки для избежания повторного поиска.
import asyncio
import aiohttp
import asyncpg
import json
# Улучшите предыдущую программу, сделав параллельно запрос к публичному веб-серверу и к любой публичной базе данных с распечаткой
# результатов этих вызовов. Например сделайте запросы к публичному
# ресурсу последовательностей РНК - RNACentral: https://rnacentral.
# org/about-us.
WEB_SERVER_URL = "https://rnacentral.org/api/v1/rna/"
DB_CONNECTION_STRING = "postgres://reader:NWDMCE5xdipIjRrp@hh-pgsql-public.ebi.ac.uk:5432/pfmegrnargs"
async def fetch_web_data():
async with aiohttp.ClientSession() as session:
async with session.get(WEB_SERVER_URL) as response:
data = await response.text()
json_data = json.loads(data)
return json_data
async def fetch_db_data():
conn = await asyncpg.connect(DB_CONNECTION_STRING)
query = "SELECT * FROM rnc_database"
result = await conn.fetch(query)
await conn.close()
return result
async def main():
web_data = await fetch_web_data()
db_data = await fetch_db_data()
print("Web Data:")
print(json.dumps(web_data, indent=4))
print("\nDatabase Data:")
for row in db_data:
print(row)
if __name__ == "__main__":
asyncio.run(main())
#Создайте асинхронный веб-скрапер, используя aiohttp и asyncio, который собирает информацию из нескольких веб-страниц одновременно.
# Вынесите код скрапера в отдельный класс. Загружайте список URLs из отдельного файла.
import aiohttp
import asyncio
class WebScraper:
def __init__(self, urls_file):
self.urls_file = urls_file
async def fetch_url(self, session, url):
async with session.get(url) as response:
return await response.text()
async def scrape(self, url):
async with aiohttp.ClientSession() as session:
html = await self.fetch_url(session, url)
print(html)
async def scrape_all(self):
tasks = []
with open(self.urls_file, 'r') as file:
urls = file.read().splitlines()
for url in urls:
task = asyncio.create_task(self.scrape(url))
tasks.append(task)
await asyncio.gather(*tasks)
if __name__ == "__main__":
urls_file = "urls.txt"
scraper = WebScraper(urls_file)
asyncio.run(scraper.scrape_all())
# Улучшите асинхронный веб-скрапер из предыдущей задачи. Преобразуйте его в класс асинхронного менеджера контекста
# и используйте вместе с async with изнутри асинхронной функции main, которая
# вызывается через asyncio.run(main()).
import aiohttp
import asyncio
from bs4 import BeautifulSoup
class AsyncWebScraper:
def __init__(self, urls_file):
self.urls_file = urls_file
self.urls = self.load_urls()
def load_urls(self):
with open(self.urls_file, 'r') as file:
return [line.strip() for line in file]
async def fetch(self, session, url):
async with session.get(url) as response:
return await response.text()
async def scrape_page(self, session, url):
html = await self.fetch(session, url)
soup = BeautifulSoup(html, 'html.parser')
title = soup.title.string
print(f"Title from {url}: {title}")
async def __aenter__(self):
self.session = aiohttp.ClientSession()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.session.close()
async def scrape_all(self):
tasks = [self.scrape_page(self.session, url) for url in self.urls]
await asyncio.gather(*tasks)
async def main():
async with AsyncWebScraper("urls.txt") as scraper:
await scraper.scrape_all()
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import time
# Создайте простую асинхронную функцию, которая в бесконечном цикле отображает текущее время с интервалом 1 секунда, и запустите ее с помощью asyncio.
# Завершать программу можно через комбинацию клавиш типа Ctrl + C или Ctrl + Break.
async def display_time():
try:
while True:
current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"Текущее время: {current_time}")
await asyncio.sleep(1)
except KeyboardInterrupt:
pass
async def main():
await display_time()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
except KeyboardInterrupt:
pass
finally:
loop.close()
import asyncio
import time
from termcolor import colored
from pynput import keyboard
# Улучшите предыдущую программу. Распечатывайте текущие дату и время в одной и той же строке (используйте символ возврата каретки '\r') разными цветами с помощью библиотеки termcolor.
# Добавьте обработку события нажатия клавиши Esc с помощью библиотеки pynput, чтобы можно было выйти из программы.
async def display_time():
try:
while True:
current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
colored_time = colored(current_time, "blue")
print(f"\rТекущая дата и время: {colored_time}", end="")
await asyncio.sleep(1)
except asyncio.CancelledError:
pass
async def main():
task = asyncio.create_task(display_time())
try:
await task
except KeyboardInterrupt:
pass
finally:
task.cancel()
def on_key_release(key):
if key == keyboard.Key.esc:
print("\nВыход из программы.")
loop.stop()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
with keyboard.Listener(on_release=on_key_release) as listener:
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
loop.close()
\ No newline at end of file
import asyncio
# Разработайте программу, которая использует asyncio.gather для выполнения двух асинхронных задач параллельно
# и затем обрабатывает результаты в отдельной функции в цикле.
async def task1():
await asyncio.sleep(2)
return "Результат задачи 1"
async def task2():
await asyncio.sleep(1)
return "Результат задачи 2"
def process_results(results):
for result in results:
print(f"Получен результат: {result}")
async def main():
results = await asyncio.gather(task1(), task2())
process_results(results)
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import json
async def main():
async with aiohttp.ClientSession() as session:
while True:
message = input("Введите сообщение (или 'exit' для завершения): ")
if message == 'exit':
break
data = {"message": message}
async with session.post('http://localhost:8080/echo', json=data) as response:
response_data = await response.json()
print("Получен ответ:", response_data["message"])
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
import aiohttp
from aiohttp import web
async def echo_handler(request):
data = await request.json()
response_data = {"message": data["message"]}
return web.json_response(response_data)
app = web.Application()
app.router.add_post('/echo', echo_handler)
web.run_app(app, host='localhost', port=8080)
Комплект 1: Библиотека Asyncio
1.1
Создайте простую асинхронную функцию, которая в бесконечном цикле отображает текущее время с интервалом 1 секунда, и запустите ее с помощью asyncio. Завершать программу можно через комбинацию клавиш типа Ctrl + C или Ctrl + Break.
1.2
Улучшите предыдущую программу. Распечатывайте текущие дату и время в одной и той же строке (используйте символ возврата каретки '\r') разными цветами с помощью библиотеки termcolor. Добавьте обработку события нажатия клавиши Esc с помощью библиотеки pynput, чтобы можно было выйти из программы.
1.3
Разработайте программу, которая использует asyncio.gather для выполнения двух асинхронных задач параллельно и затем обрабатывает результаты в отдельной функции в цикле.
1.4
Улучшите предыдущую программу, сделав параллельно запрос к публичному веб-серверу и к любой публичной базе данных с распечаткой результатов этих вызовов. Например, сделайте запросы к публичному ресурсу последовательностей РНК – RNACentral.
1.5
Создайте асинхронный веб-скрапер, используя aiohttp и asyncio, который собирает информацию из нескольких веб-страниц одновременно. Вынесите код скрапера в отдельный класс. Загружайте список URLs из отдельного файла.
1.6
Улучшите асинхронный веб-скрапер из предыдущей задачи. Преобразуйте его в класс асинхронного менеджера контекста и используйте вместе с async with изнутри асинхронной функции main, которая вызывается через asyncio.run(main()).
1.7
Реализуйте сервер и клиент с использованием библиотек asyncio и HTTPS протокола для отправки текстовых JSON сообщений, позволяющий передавать текстовые сообщения в обе стороны в цикле с клавиатуры в режиме echo.
\ No newline at end of file
https://docs.python.org/3/
https://habr.com/ru/companies/simbirsoft/articles/701020/
https://tproger.ru/translations/asynchronous-programming-in-python
\ No newline at end of file
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
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?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
"""
ASGI config for mysite project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
application = get_asgi_application()
"""
Django settings for mysite project.
Generated by 'django-admin startproject' using Django 4.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-_l+pqxv$*jyi93923^e%#10-$p*v)#c)e&z$&&72^9!4x#=3kw"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"corsheaders",
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'crispy_forms',
'crispy_bootstrap5',
'rest_framework',
'debug_toolbar',
'django_extensions',
"polls.apps.PollsConfig",
]
SITE_ID = 2 # !
SOCIALACCOUNT_LOGIN_ON_GET = True
SOCIALACCOUNT_AUTO_SIGNUP = True
SOCIALACCOUNT_QUERY_EMAIL = True
ACCOUNT_LOGOUT_ON_GET= True # !
ACCOUNT_UNIQUE_EMAIL = True # !
ACCOUNT_EMAIL_REQUIRED = True # !
# LOGIN_REDIRECT_URL = '/'
# LOGOUT_REDIRECT_URL = '/'
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"allauth.account.middleware.AccountMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
ROOT_URLCONF = "mysite.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "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",
],
},
},
]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
WSGI_APPLICATION = "mysite.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
DEFAULT_FROM_EMAIL = 'polls-from-the-crypt@mail.ru'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.mail.ru'
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_HOST_USER = os.environ.get('POLLS_EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('POLLS_EMAIL_HOST_PASSWORD')
# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/Moscow"
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = (
('en-us', 'English'),
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTHENTICATION_BACKENDS = [
'allauth.account.auth_backends.AuthenticationBackend'
]
SOCIALACCOUNT_PROVIDERS = {
'google': {
"APP": {
"client_id": "106106814332-su7fmhvccp9r885cukduov88puo5566f.apps.googleusercontent.com",
"secret": "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",
"key": ""
},
'SCOPE': [
'profile',
'email',
],
'AUTH_PARAMS': {
'access_type': 'online',
}
}
}
# Debug Toolbar
INTERNAL_IPS = [
# ...
"127.0.0.1",
# ...
]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}
# SHOW_TOOLBAR_CALLBACK = 'debug_toolbar.middleware.show_toolbar'
"""
URL configuration for mysite project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("", include("polls.urls", namespace='polls')),
path("admin/", admin.site.urls),
path('accounts/', include('allauth.urls')),
path("__debug__/", include("debug_toolbar.urls")),
]
"""
WSGI config for mysite 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/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
application = get_wsgi_application()
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
]
inlines = [ChoiceInline]
list_display = ["question_text", "pub_date", "was_published_recently"]
list_filter = ["pub_date"]
search_fields = ["question_text"]
admin.site.register(Question, QuestionAdmin)
from django.apps import AppConfig
class PollsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "polls"
from django import forms
from django.utils import timezone
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.contrib.auth.tokens import default_token_generator
from .models import Question, Choice
class QuestionForm(forms.ModelForm):
choices = forms.CharField(
label='Question Choices',
widget=forms.Textarea(attrs={'rows': 7}),
help_text='Enter the answer options, separating each option with a new line.'
)
class Meta:
model = Question
fields = ['question_text', 'choices']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
choices = '\n'.join(self.instance.choice_set.values_list('choice_text', flat=True))
self.initial['choices'] = choices
def clean_choices(self):
choices = self.cleaned_data.get('choices')
if choices:
# Разбить строки по переносу строки и удалить пустые элементы
choices_list = [choice.strip() for choice in choices.split('\n') if choice.strip()]
# Проверить, чтобы было хотя бы два варианта ответа
if len(choices_list) < 2:
raise forms.ValidationError('Enter at least two answer options.')
return choices_list
return []
def save(self, commit=True):
question_instance = super().save(commit=False)
if not question_instance.pub_date:
question_instance.pub_date = timezone.now()
question_instance.save()
Choice.objects.filter(question=question_instance).delete()
choices = self.cleaned_data.get('choices')
if choices:
for choice_text in choices:
Choice.objects.create(question=question_instance, choice_text=choice_text)
return question_instance
class NewUserForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ("username", "email", "password1", "password2")
def save(self, request, commit=True):
user = super(NewUserForm, self).save(commit=False)
user.email = self.cleaned_data['email']
user.is_active = False
user.is_superuser = False
user.is_staff = False
if commit:
user.save()
self.send_verification_email(user, request)
return user
def send_verification_email(self, user, request):
current_site = get_current_site(request)
mail_subject = 'Activate your account'
message = render_to_string(
'polls/verification_email.html',
{
'user': user,
'domain': current_site.domain,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': default_token_generator.make_token(user),
}
)
email = EmailMessage(mail_subject, message, to=[user.email])
email.content_subtype = "html"
email.send()
# Generated by Django 4.2.6 on 2023-10-11 12:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Question",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("question_text", models.CharField(max_length=200)),
("pub_date", models.DateTimeField(verbose_name="date published")),
],
),
migrations.CreateModel(
name="Choice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("choice_text", models.CharField(max_length=200)),
("votes", models.IntegerField(default=0)),
(
"question",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="polls.question"
),
),
],
),
]
from django.db import models
from django.utils import timezone
from django.contrib import admin
import datetime
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published", blank=True, null=True)
@admin.display(
boolean=True,
ordering="pub_date",
description="Published recently?",
)
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
def __str__(self):
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text
from rest_framework import serializers
from .models import Question
class QuestionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField()
class Meta:
model = Question
fields = '__all__'
@font-face {
font-family: 'Royal';
src: url('fonts/Royal.ttf');
}
@font-face {
font-family: 'CloisterBlack-Light';
src: url('fonts/CloisterBlackLight.ttf');
}
@font-face {
font-family: 'WitcherHandwriting-Regular';
src: url('fonts/WitcherHandwriting-Regular.otf') format("opentype");
}
@font-face {
font-family: 'HalloweenNightmare';
src: url('fonts/HalloweenNightmare.otf') format("opentype");
}
body {
background: white url("images/background.png") no-repeat;
}
.page-header {
background-color: #aa00ff;
margin-top: 0;
padding: 20px 20px 20px 40px;
}
.page-header h1, .page-header h1 a, .page-header h1 a:visited, .page-header h1 a:active,
.page-header h2, .page-header h2 a, .page-header h2 a:visited, .page-header h2 a:active,
.page-header h3, .page-header h3 a, .page-header h3 a:visited, .page-header h3 a:active
.page-header h4, .page-header h4 a, .page-header h4 a:visited, .page-header h4 a:active {
color: #ffffff;
text-decoration: none;
}
.page-header h1 {
font-size: 48pt;
font-family: 'CloisterBlack-Light'
}
.page-header h4 {
font-size: 24pt;
font-family: 'WitcherHandwriting-Regular';
}
.content {
margin-left: 40px;
}
h1, h2, h3, h4 {
font-family: Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif;
}
.poll h4, .poll h4 a, .pollr h4 a:visited, .poll h4 a:active {
color: #000000;
font-family: 'HalloweenNightmare';
text-decoration: none;
font-size: 28pt;
}
.content legend h1 {
color: #000000;
font-family: 'HalloweenNightmare';
text-decoration: none;
font-size: 32pt;
}
.content form fieldset label {
color: #000000;
font-family: 'HalloweenNightmare';
font-size: 20pt;
}
.content form .btn-dark {
color: #ffffff;
font-family: 'HalloweenNightmare';
font-size: 20pt;
}
.date {
color: #535353;
font-family: 'HalloweenNightmare';
font-size: 16pt;
}
.save {
float: right;
}
.poll-form textarea, .poll-form input {
width: 100%;
}
.top-menu, .top-menu:hover, .top-menu:visited {
color: #ffffff;
float: right;
font-size: 26pt;
margin-right: 20px;
}
.top-menu-welcome, .top-menu-welcome:hover, .top-menu-welcome:visited {
color: #ffffff;
float: right;
font-size: 13pt;
margin-right: 20px;
text-decoration: none;
}
.poll {
margin-bottom: 70px;
}
.poll h1 a, .poll h1 a:visited {
color: #000000;
}
li a {
color: green;
}
{% extends "polls/base.html" %}
{% block content %}
<h1>Account Activation Failed</h1>
<p>We were unable to activate your account. The activation link is invalid or has expired.</p>
<p>Please <a href="{% url 'polls:register' %}">register</a> again or contact support if you need assistance.</p>
{% endblock %}
{% extends "polls/base.html" %}
{% block content %}
<h1>Account Activation Successful</h1>
<p>Congratulations! Your account has been successfully activated.</p>
<p>You may now <a href="{% url 'polls:login' %}">login</a> to start using your account. Enjoy!</p>
{% endblock %}
{% load static %}
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Polls</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}" />
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Poll Application</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li><a href="#">Home</a></li>
<li><a href="#">Create</a></li>
{% if user.is_authenticated %}
{% if user.is_staff %}
<li><a href="{% url 'polls:poll_new' %}" class="top-menu">New</a></li>
{% endif %}
<li><a href="{% url 'polls:logout' %}" class="top-menu">Logout</a></li>
<a href="#" class="top-menu-welcome">
<span style="color: white">Welcome, {{user.username}}</span></a>
{% else %}
<li><a href="{% url 'polls:register' %}" class="top-menu">Register</a></li>
<li><a href="{% url 'polls:login' %}" class="top-menu">Login</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div>
</nav>
<!-- Messages -->
<div class="container">
{% if messages %}
<div class="alert alert-info alert-dismissible fade show" role="alert">
{% for message in messages %}
{{ message }}
{% endfor %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
</div>
<div class="content container mt-4">
<div class="row">
<div class="col-md-8">
{% block content %}
{% endblock %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
{% extends 'polls/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend>
<h1>{{ question.question_text }}</h1>
{% if question.pub_date %}
<div class="date">
{{ question.pub_date }}
</div>
{% endif %}
{% if user.is_staff %}
<a class="btn btn-default" href="{% url 'polls:poll_edit' pk=question.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
</a>
{% endif %}
</legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" class="btn btn-dark" value="Vote">
</form>
{% endblock %}
\ No newline at end of file
{% extends 'polls/base.html' %}
{% block content %}
{% if latest_question_list %}
{% for question in latest_question_list %}
<div class="poll">
<h4><a href="{% url 'polls:detail' pk=question.pk %}">{{ question.question_text|linebreaksbr }}</a></h4>
<div class="date">
<p>Date: '{{ question.pub_date }}'</p>
</div>
<hr>
</div>
{% endfor %}
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endblock %}
\ No newline at end of file
{% extends "polls/base.html" %}
{% block content %}
{% load socialaccount %}
{% load crispy_forms_tags %}
<!--Login-->
<div class="py-5">
<h1>Login</h1> {{slogan}}
<form method="POST">
{% csrf_token %}
{{ login_form|crispy }}
<button class="btn btn-primary" type="submit">Login</button>
</form>
<p class="text-center">Don't have an account? <a href="{% url 'polls:register' %}">Create an account</a>.</p>
</div>
<img src="https://developers.google.com/identity/images/g-logo.png" alt="Google Icon">
<a href="{% provider_login_url 'google' %}?next=/">
Login with Google
</a>
{% endblock %}
\ No newline at end of file
{% extends 'polls/base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<h1>Poll</h1>
<form method="POST" class="poll-form">{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="save btn btn-info">Save</button>
</form>
{% endblock %}
{% extends "polls/base.html" %}
{% block content %}
{% load crispy_forms_tags %}
<div class="py-5">
<h1>Register</h1>
<form method="POST">
{% csrf_token %}
{{ register_form|crispy }}
<button class="btn btn-primary" type="submit">Register</button>
</form>
<p class="text-center">If you already have an account, <a href="{% url 'polls:login' %}">login</a> instead.</p>
</div>
{% endblock %}
\ No newline at end of file
{% extends 'polls/base.html' %}
{% block content %}
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Account Verification</title>
</head>
<body>
<p>Dear {{ user.username }},</p>
<p>Thank you for creating an account. Please click the link below to activate your account:</p>
<a href="http://{{ domain }}{% url 'polls:activate' uidb64=uid token=token %}">Activate Account</a>
<p>If you did not create this account, you can safely ignore this email.</p>
<p>Best regards,</p>
<p>Your Polls from the Crypt</p>
</body>
</html>
\ No newline at end of file
import datetime
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from .models import Question
def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is displayed.
"""
response = self.client.get(reverse("polls:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question2, question1],
)
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
The detail view of a question with a pub_date in the future
returns a 404 not found.
"""
future_question = create_question(question_text="Future question.", days=5)
url = reverse("polls:detail", args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
"""
The detail view of a question with a pub_date in the past
displays the question's text.
"""
past_question = create_question(question_text="Past Question.", days=-5)
url = reverse("polls:detail", args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
\ No newline at end of file
from django.urls import path, include
from . import views
app_name = 'polls'
urlpatterns = [
# ex: /polls/
path("", views.IndexView.as_view(), name="index"),
# ex: /polls/5/
path("<int:pk>/", views.DetailView.as_view(), name="detail"),
# ex: /polls/5/results/
path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
# ex: /polls/5/vote/
path("<int:question_id>/vote/", views.VoteView.as_view(), name="vote"),
path("new/", views.PollNewView.as_view(), name="poll_new"),
# ex: /polls/5/edit/
path('<int:pk>/edit/', views.PollEditView.as_view(), name='poll_edit'),
path("login", views.LoginView.as_view(), name="login"),
path("logout", views.LogoutView.as_view(), name="logout"),
path("register", views.AccountRegisterView.as_view(), name="register"),
path("activate/<uidb64>/<token>", views.AccountActivationView.as_view(), name="activate"),
]
This diff is collapsed.
{% extends "admin/base.html" %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}
{% block nav-global %}{% endblock %}
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/dashboard.css" %}">{% endblock %}
{% block coltype %}colMS{% endblock %}
{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
{% block nav-breadcrumbs %}{% endblock %}
{% block nav-sidebar %}{% endblock %}
{% block content %}
<div id="content-main">
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
</div>
{% endblock %}
{% block sidebar %}
<div id="content-related">
<div class="module" id="recent-actions-module">
<h2>{% translate 'Recent actions' %}</h2>
<h3>{% translate 'My actions' %}</h3>
{% load log %}
{% get_admin_log 10 as admin_log for_user user %}
{% if not admin_log %}
<p>{% translate 'None available' %}</p>
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
{% if entry.is_deletion or not entry.get_admin_url %}
{{ entry.object_repr }}
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
<br>
{% if entry.content_type %}
<span class="mini quiet">{% filter capfirst %}{{ entry.content_type.name }}{% endfilter %}</span>
{% else %}
<span class="mini quiet">{% translate 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endblock %}
Данный проект сделан по примеру из официального туториала:
https://docs.djangoproject.com/en/4.2/intro/tutorial01/
Проект состоит из двух частей:
Общедоступный сайт, который позволяет людям просматривать опросы и голосовать в них. http://127.0.0.1:8000/polls/
Сайт администратора, который позволяет добавлять, изменять и удалять опросы. http://127.0.0.1:8000/admin/
Есть возможность залогиниться через гугл.
asgiref==3.7.2
blessed==1.20.0
bpython==0.24
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.0
contourpy==1.2.0
crispy-bootstrap5==0.7
cryptography==41.0.5
curtsies==0.4.2
cwcwidth==0.1.9
cycler==0.12.1
defusedxml==0.7.1
Django==4.2.7
django-allauth==0.58.2
django-cors-headers==4.3.1
django-crispy-forms==2.1
django-debug-toolbar==4.2.0
django-extensions==3.2.3
djangorestframework==3.14.0
fonttools==4.45.0
greenlet==3.0.0
idna==3.4
kiwisolver==1.4.5
Markdown==3.5
MarkupSafe==2.1.3
matplotlib==3.8.2
numpy==1.26.2
oauthlib==3.2.2
packaging==23.2
Pillow==10.1.0
pycparser==2.21
Pygments==2.16.1
PyJWT==2.8.0
pyparsing==3.1.1
python-dateutil==2.8.2
python3-openid==3.2.0
pytz==2023.3.post1
pyxdg==0.28
requests==2.31.0
requests-oauthlib==1.3.1
six==1.16.0
sqlparse==0.4.4
urllib3==2.0.6
wcwidth==0.2.8
Werkzeug==3.0.1
# pull the official base image
FROM python:3.8.3-alpine
# set environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# set work directory
WORKDIR /code
# install dependencies
COPY requirements.txt /code/
RUN pip install -r requirements.txt
# copy project
COPY . /code/
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
include LICENSE
include README.rst
recursive-include polls/static *
recursive-include polls/templates *
recursive-include docs *
\ No newline at end of file
=====
Создание переиспользуемого приложения на Django
=====
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
]
inlines = [ChoiceInline]
list_display = ["question_text", "pub_date", "was_published_recently"]
list_filter = ["pub_date"]
search_fields = ["question_text"]
admin.site.register(Question, QuestionAdmin)
from django.apps import AppConfig
class PollsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "polls"
from django import forms
from django.utils import timezone
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.contrib.auth.tokens import default_token_generator
from .models import Question, Choice
class QuestionForm(forms.ModelForm):
choices = forms.CharField(
label='Question Choices',
widget=forms.Textarea(attrs={'rows': 7}),
help_text='Enter the answer options, separating each option with a new line.'
)
class Meta:
model = Question
fields = ['question_text', 'choices']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
choices = '\n'.join(self.instance.choice_set.values_list('choice_text', flat=True))
self.initial['choices'] = choices
def clean_choices(self):
choices = self.cleaned_data.get('choices')
if choices:
# Разбить строки по переносу строки и удалить пустые элементы
choices_list = [choice.strip() for choice in choices.split('\n') if choice.strip()]
# Проверить, чтобы было хотя бы два варианта ответа
if len(choices_list) < 2:
raise forms.ValidationError('Enter at least two answer options.')
return choices_list
return []
def save(self, commit=True):
question_instance = super().save(commit=False)
if not question_instance.pub_date:
question_instance.pub_date = timezone.now()
question_instance.save()
Choice.objects.filter(question=question_instance).delete()
choices = self.cleaned_data.get('choices')
if choices:
for choice_text in choices:
Choice.objects.create(question=question_instance, choice_text=choice_text)
return question_instance
class NewUserForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ("username", "email", "password1", "password2")
def save(self, request, commit=True):
user = super(NewUserForm, self).save(commit=False)
user.email = self.cleaned_data['email']
user.is_active = False
user.is_superuser = False
user.is_staff = False
if commit:
user.save()
self.send_verification_email(user, request)
return user
def send_verification_email(self, user, request):
current_site = get_current_site(request)
mail_subject = 'Activate your account'
message = render_to_string(
'polls/verification_email.html',
{
'user': user,
'domain': current_site.domain,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': default_token_generator.make_token(user),
}
)
email = EmailMessage(mail_subject, message, to=[user.email])
email.content_subtype = "html"
email.send()
# Generated by Django 4.2.6 on 2023-10-11 12:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Question",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("question_text", models.CharField(max_length=200)),
("pub_date", models.DateTimeField(verbose_name="date published")),
],
),
migrations.CreateModel(
name="Choice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("choice_text", models.CharField(max_length=200)),
("votes", models.IntegerField(default=0)),
(
"question",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="polls.question"
),
),
],
),
]
from django.db import models
from django.utils import timezone
from django.contrib import admin
import datetime
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published", blank=True, null=True)
@admin.display(
boolean=True,
ordering="pub_date",
description="Published recently?",
)
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
def __str__(self):
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text
@font-face {
font-family: 'Royal';
src: url('fonts/Royal.ttf');
}
@font-face {
font-family: 'CloisterBlack-Light';
src: url('fonts/CloisterBlackLight.ttf');
}
@font-face {
font-family: 'WitcherHandwriting-Regular';
src: url('fonts/WitcherHandwriting-Regular.otf') format("opentype");
}
@font-face {
font-family: 'HalloweenNightmare';
src: url('fonts/HalloweenNightmare.otf') format("opentype");
}
body {
background: white url("images/background.png") no-repeat;
}
.page-header {
background-color: #aa00ff;
margin-top: 0;
padding: 20px 20px 20px 40px;
}
.page-header h1, .page-header h1 a, .page-header h1 a:visited, .page-header h1 a:active,
.page-header h2, .page-header h2 a, .page-header h2 a:visited, .page-header h2 a:active,
.page-header h3, .page-header h3 a, .page-header h3 a:visited, .page-header h3 a:active
.page-header h4, .page-header h4 a, .page-header h4 a:visited, .page-header h4 a:active {
color: #ffffff;
text-decoration: none;
}
.page-header h1 {
font-size: 48pt;
font-family: 'CloisterBlack-Light'
}
.page-header h4 {
font-size: 24pt;
font-family: 'WitcherHandwriting-Regular';
}
.content {
margin-left: 40px;
}
h1, h2, h3, h4 {
font-family: Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif;
}
.poll h4, .poll h4 a, .pollr h4 a:visited, .poll h4 a:active {
color: #000000;
font-family: 'HalloweenNightmare';
text-decoration: none;
font-size: 28pt;
}
.content legend h1 {
color: #000000;
font-family: 'HalloweenNightmare';
text-decoration: none;
font-size: 32pt;
}
.content form fieldset label {
color: #000000;
font-family: 'HalloweenNightmare';
font-size: 20pt;
}
.content form .btn-dark {
color: #ffffff;
font-family: 'HalloweenNightmare';
font-size: 20pt;
}
.date {
color: #535353;
font-family: 'HalloweenNightmare';
font-size: 16pt;
}
.save {
float: right;
}
.poll-form textarea, .poll-form input {
width: 100%;
}
.top-menu, .top-menu:hover, .top-menu:visited {
color: #ffffff;
float: right;
font-size: 26pt;
margin-right: 20px;
}
.top-menu-welcome, .top-menu-welcome:hover, .top-menu-welcome:visited {
color: #ffffff;
float: right;
font-size: 13pt;
margin-right: 20px;
text-decoration: none;
}
.poll {
margin-bottom: 70px;
}
.poll h1 a, .poll h1 a:visited {
color: #000000;
}
li a {
color: green;
}
{% extends "polls/base.html" %}
{% block content %}
<h1>Account Activation Failed</h1>
<p>We were unable to activate your account. The activation link is invalid or has expired.</p>
<p>Please <a href="{% url 'polls:register' %}">register</a> again or contact support if you need assistance.</p>
{% endblock %}
{% extends "polls/base.html" %}
{% block content %}
<h1>Account Activation Successful</h1>
<p>Congratulations! Your account has been successfully activated.</p>
<p>You may now <a href="{% url 'polls:login' %}">login</a> to start using your account. Enjoy!</p>
{% endblock %}
{% load static %}
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Polls from the Crypt</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'polls/style.css' %}">
</head>
<body>
<div class="page-header">
{% if user.is_authenticated %}
{% if user.is_staff %}
<a href="{% url 'polls:poll_new' %}" class="top-menu">
<span style="color: white">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</span>
</a>
{% endif %}
<a href="{% url 'polls:logout' %}" class="top-menu">
<span style="color: white">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v2a.5.5 0 0 1-1 0v-2A1.5 1.5 0 0 1 6.5 2h8A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 5 12.5v-2a.5.5 0 0 1 1 0v2z"/>
<path fill-rule="evenodd" d="M.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L1.707 7.5H10.5a.5.5 0 0 1 0 1H1.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3z"/>
</svg>
</span>
</a>
<a href="#" class="top-menu-welcome">
<span style="color: white">Welcome, {{user.username}}</span>
</a>
{% else %}
<div style="vertical-align: middle">
<a href="{% url 'polls:register' %}" class="top-menu">
<span style="color: white">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-box-arrow-in-up" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.5 10a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 0 0 1h2A1.5 1.5 0 0 0 14 9.5v-8A1.5 1.5 0 0 0 12.5 0h-9A1.5 1.5 0 0 0 2 1.5v8A1.5 1.5 0 0 0 3.5 11h2a.5.5 0 0 0 0-1h-2z"/>
<path fill-rule="evenodd" d="M7.646 4.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V14.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708l3-3z"/>
</svg>
</span>
</a>
<a href="{% url 'polls:login' %}" class="top-menu">
<span style="color: white">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-box-arrow-in-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z"/>
<path fill-rule="evenodd" d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg>
</span>
</a>
</div>
{% endif %}
<h1><a href="/polls">Polls from the Crypt</a></h1>
<h4><a href="/polls">{{ slogan }}</a></h4>
</div>
<!-- Messages -->
<div class="container">
{% if messages %}
<div class="alert alert-info alert-dismissible fade show" role="alert">
{% for message in messages %}
{{ message }}
{% endfor %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
</div>
<div class="content container mt-4">
<div class="row">
<div class="col-md-8">
{% block content %}
{% endblock %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
{% extends 'polls/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend>
<h1>{{ question.question_text }}</h1>
{% if question.pub_date %}
<div class="date">
{{ question.pub_date }}
</div>
{% endif %}
{% if user.is_staff %}
<a class="btn btn-default" href="{% url 'polls:poll_edit' pk=question.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
</a>
{% endif %}
</legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" class="btn btn-dark" value="Vote">
</form>
{% endblock %}
\ No newline at end of file
{% extends 'polls/base.html' %}
{% block content %}
{% if latest_question_list %}
{% for question in latest_question_list %}
<div class="poll">
<h4><a href="{% url 'polls:detail' pk=question.pk %}">{{ question.question_text|linebreaksbr }}</a></h4>
<div class="date">
<p>Date: '{{ question.pub_date }}'</p>
</div>
<hr>
</div>
{% endfor %}
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endblock %}
\ No newline at end of file
{% extends "polls/base.html" %}
{% block content %}
{% load socialaccount %}
{% load crispy_forms_tags %}
<!--Login-->
<div class="py-5">
<h1>Login</h1> {{slogan}}
<form method="POST">
{% csrf_token %}
{{ login_form|crispy }}
<button class="btn btn-primary" type="submit">Login</button>
</form>
<p class="text-center">Don't have an account? <a href="{% url 'polls:register' %}">Create an account</a>.</p>
</div>
<div class="container">
<a href="/admin/google/login" id="google-signin-button">
<img src="https://developers.google.com/identity/images/g-logo.png" alt="Google Icon">
<span class="text">Sign in with Google</span>
</a>
</div>
{% endblock %}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment