mirror of
https://github.com/aykhans/series-robot-web.git
synced 2025-06-07 00:29:01 +00:00
first commit
This commit is contained in:
commit
f9930c2476
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
migrations
|
||||||
|
db.sqlite3
|
||||||
|
src/config/settings/static
|
||||||
|
.env
|
4
src/.env.example
Normal file
4
src/.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
SECRET_KEY=
|
||||||
|
DB_NAME=
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
1
src/API_client/__init__.py
Normal file
1
src/API_client/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .get_request import APIClient
|
33
src/API_client/get_request.py
Normal file
33
src/API_client/get_request.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from requests import get
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class APIClient:
|
||||||
|
def __init__(self, api_key: str) -> None:
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
|
def __get_data(self, link: str) -> dict:
|
||||||
|
raw_data = get(link)
|
||||||
|
if raw_data.status_code == 200:
|
||||||
|
data = raw_data.json()
|
||||||
|
|
||||||
|
if data['errorMessage'] == 'Invalid API Key': raise ValidationError('Invalid API Key')
|
||||||
|
elif data['errorMessage']: raise ValidationError('id is not correct.')
|
||||||
|
|
||||||
|
return data
|
||||||
|
raise ValidationError('Could not created. Please try again later.')
|
||||||
|
|
||||||
|
def key_control(self) -> None:
|
||||||
|
self.__get_data(f"https://imdb-api.com/en/API/Title/{self.api_key}/tt7939766")
|
||||||
|
|
||||||
|
def get_seasons(self, series_id: str) -> list:
|
||||||
|
data = self.__get_data(f"https://imdb-api.com/en/API/Title/{self.api_key}/{series_id}")
|
||||||
|
|
||||||
|
if not data['tvSeriesInfo']:
|
||||||
|
raise ValidationError('This is not a TV series id')
|
||||||
|
return data['tvSeriesInfo']['seasons']
|
||||||
|
|
||||||
|
def get_episodes_count(self, season: int, series_id: str) -> int:
|
||||||
|
data = self.__get_data(
|
||||||
|
f"https://imdb-api.com/en/API/SeasonEpisodes/{self.api_key}/{series_id}/{season}")
|
||||||
|
return len(data['episodes'])
|
0
src/account/__init__.py
Normal file
0
src/account/__init__.py
Normal file
13
src/account/admin.py
Normal file
13
src/account/admin.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class CustomAdmin(UserAdmin):
|
||||||
|
list_display = ('username', 'email')
|
||||||
|
fieldsets = UserAdmin.fieldsets + (
|
||||||
|
('IMDB API Key Field', {
|
||||||
|
'fields': ['imdb_api_key']
|
||||||
|
}),
|
||||||
|
)
|
6
src/account/apps.py
Normal file
6
src/account/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'account'
|
2
src/account/forms/__init__.py
Normal file
2
src/account/forms/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .register import RegisterForm
|
||||||
|
from .profile_editing import ProfileEditingForm
|
10
src/account/forms/profile_editing.py
Normal file
10
src/account/forms/profile_editing.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.contrib.auth.forms import UserChangeForm
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileEditingForm(UserChangeForm):
|
||||||
|
password = None
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('email', 'username', 'imdb_api_key')
|
14
src/account/forms/register.py
Normal file
14
src/account/forms/register.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterForm(UserCreationForm):
|
||||||
|
password = None
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('username',
|
||||||
|
'email',
|
||||||
|
'password1',
|
||||||
|
'password2',
|
||||||
|
'imdb_api_key')
|
25
src/account/models.py
Normal file
25
src/account/models.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from API_client import APIClient
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
imdb_api_key = models.CharField(max_length=15, blank=False, null=False)
|
||||||
|
|
||||||
|
def clean(self) -> None:
|
||||||
|
client = APIClient(self.imdb_api_key)
|
||||||
|
client.key_control()
|
||||||
|
# raw_data = get(f"https://imdb-api.com/en/API/Title/{self.imdb_api_key}/tt0110413")
|
||||||
|
|
||||||
|
# if raw_data.status_code == 200: data = raw_data.json()
|
||||||
|
# else: raise ValidationError('Account not created. Please try again later')
|
||||||
|
# if not data['errorMessage']: raise ValidationError(data['errorMessage'])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'user'
|
||||||
|
verbose_name = 'User'
|
||||||
|
verbose_name_plural = 'Users'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
23
src/account/templates/change_password.html
Normal file
23
src/account/templates/change_password.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %} Change Password {% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.media}}
|
||||||
|
{{form|crispy}}
|
||||||
|
<style>
|
||||||
|
.btn:hover {
|
||||||
|
background: #fff;
|
||||||
|
color: #0069D9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<input type="submit" value="Change Password" class="btn btn-primary mt-3 mb-5">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
24
src/account/templates/login.html
Normal file
24
src/account/templates/login.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %} Login {% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'components/message.html' %}
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form|crispy}}
|
||||||
|
<style>
|
||||||
|
.btn:hover {
|
||||||
|
background: #fff;
|
||||||
|
color: #0069D9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<input type="submit" value="Login" class="btn btn-primary mt-3 mb-3">
|
||||||
|
<p class="mb-5">No account? <a href="{% url 'register' %}">Signup</a></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
24
src/account/templates/profile_detail.html
Normal file
24
src/account/templates/profile_detail.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %} Profile {% endblock title %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Username: <small>{{profile.username}}</small></h5>
|
||||||
|
<h5>Email: <small>{{profile.email|default:""}}</small></h5>
|
||||||
|
<h5>IMDB API Key: <small>{{profile.imdb_api_key}}</small></h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/account/profile-editing" style="color: inherit; text-decoration: none;">
|
||||||
|
<button type="button" class="btn btn-primary">Edit Profile</button>
|
||||||
|
</a>
|
||||||
|
<a href="/account/change-password" style="color: inherit; text-decoration: none;">
|
||||||
|
<button type="button" class="btn btn-primary">Change Password</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endblock content %}
|
23
src/account/templates/profile_editing.html
Normal file
23
src/account/templates/profile_editing.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %} Profil Güncəllə {% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.media}}
|
||||||
|
{{form|crispy}}
|
||||||
|
<style>
|
||||||
|
.btn:hover {
|
||||||
|
background: #fff;
|
||||||
|
color: #0069D9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<input type="submit" value="Update Profile" class="btn btn-primary mt-3 mb-5">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
25
src/account/templates/register.html
Normal file
25
src/account/templates/register.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %} Signup {% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.media}}
|
||||||
|
{{form|crispy}}
|
||||||
|
<a href="https://imdb-api.com/Identity/Account/Register">Get API Key</a><br>
|
||||||
|
<style>
|
||||||
|
.btn:hover {
|
||||||
|
background: #fff;
|
||||||
|
color: #0069D9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<input type="submit" value="Signup" class="btn btn-primary mt-3 mb-3">
|
||||||
|
<p class="mb-5">Already have an account? <a href="{% url 'login' %}">Login</a></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
3
src/account/tests.py
Normal file
3
src/account/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
15
src/account/urls.py
Normal file
15
src/account/urls.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from account import views
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('login', auth_views.LoginView.as_view(
|
||||||
|
template_name = 'login.html'
|
||||||
|
), name='login'),
|
||||||
|
path('register', views.register_view, name='register'),
|
||||||
|
path('logout', views.logout_view, name='logout'),
|
||||||
|
path('change-password', views.change_password_view, name='change-password'),
|
||||||
|
path('profile-editing', views.profile_editing_view, name='profile-editing'),
|
||||||
|
path('profile', views.ProfileDetailView.as_view(), name='profile')
|
||||||
|
]
|
5
src/account/views/__init__.py
Normal file
5
src/account/views/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .register import register_view
|
||||||
|
from .logout import logout_view
|
||||||
|
from .change_password import change_password_view
|
||||||
|
from .profile_editing import profile_editing_view
|
||||||
|
from .profile_detail import ProfileDetailView
|
22
src/account/views/change_password.py
Normal file
22
src/account/views/change_password.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
|
from django.contrib.auth import update_session_auth_hash
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url='/')
|
||||||
|
def change_password_view(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = PasswordChangeForm(request.user, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.save()
|
||||||
|
update_session_auth_hash(request, user)
|
||||||
|
|
||||||
|
messages.success(request, 'Password changed')
|
||||||
|
return redirect('homepage')
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = PasswordChangeForm(request.user)
|
||||||
|
|
||||||
|
return render(request, 'change_password.html', context={'form': form})
|
12
src/account/views/logout.py
Normal file
12
src/account/views/logout.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django.contrib.auth import logout
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url='/account/login')
|
||||||
|
def logout_view(request):
|
||||||
|
logout(request)
|
||||||
|
|
||||||
|
messages.success(request, 'Logged Out')
|
||||||
|
return redirect('login')
|
16
src/account/views/profile_detail.py
Normal file
16
src/account/views/profile_detail.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from django.views.generic import DetailView
|
||||||
|
from account.models import User
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileDetailView(LoginRequiredMixin, DetailView):
|
||||||
|
login_url = reverse_lazy('login')
|
||||||
|
template_name = 'profile_detail.html'
|
||||||
|
context_object_name = 'profile'
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return get_object_or_404(
|
||||||
|
User, id=self.request.user.id
|
||||||
|
)
|
20
src/account/views/profile_editing.py
Normal file
20
src/account/views/profile_editing.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from account.forms import ProfileEditingForm
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url='/')
|
||||||
|
def profile_editing_view(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ProfileEditingForm(request.POST, instance=request.user)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
messages.success(request, 'Profile Updated')
|
||||||
|
return redirect('homepage')
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = ProfileEditingForm(instance=request.user)
|
||||||
|
|
||||||
|
return render(request, 'profile_editing.html', context={"form": form})
|
35
src/account/views/register.py
Normal file
35
src/account/views/register.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from account.forms import RegisterForm
|
||||||
|
from django.contrib.auth import login, authenticate
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from requests import get
|
||||||
|
|
||||||
|
|
||||||
|
def register_view(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = RegisterForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
# raw_data = get(f"https://imdb-api.com/en/API/Title/{request.POST['imdb_api_key']}/tt0110413")
|
||||||
|
# if raw_data.status_code == 200: data = raw_data.json()
|
||||||
|
# else:
|
||||||
|
# messages.info(request, 'Account not created. Please try again later')
|
||||||
|
# return redirect('register')
|
||||||
|
|
||||||
|
# if data['errorMessage'] == 'Invalid API Key':
|
||||||
|
# messages.info(request, 'Invalid API Key')
|
||||||
|
# return redirect('register')
|
||||||
|
|
||||||
|
form.save()
|
||||||
|
username = form.cleaned_data.get('username')
|
||||||
|
password = form.cleaned_data.get('password1')
|
||||||
|
user = authenticate(username=username, password=password)
|
||||||
|
login(request, user)
|
||||||
|
|
||||||
|
messages.success(request, 'Registration Successful')
|
||||||
|
return redirect('homepage')
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = RegisterForm()
|
||||||
|
|
||||||
|
return render(request, 'register.html', context={"form": form})
|
0
src/config/__init__.py
Normal file
0
src/config/__init__.py
Normal file
16
src/config/asgi.py
Normal file
16
src/config/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for config 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.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
0
src/config/settings/__init__.py
Normal file
0
src/config/settings/__init__.py
Normal file
84
src/config/settings/base.py
Normal file
84
src/config/settings/base.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import environ
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
|
env = environ.Env()
|
||||||
|
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
||||||
|
|
||||||
|
SECRET_KEY = env("SECRET_KEY")
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'account',
|
||||||
|
'series',
|
||||||
|
'crispy_forms',
|
||||||
|
]
|
||||||
|
|
||||||
|
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 = 'config.urls'
|
||||||
|
|
||||||
|
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'config.wsgi.application'
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'account.User'
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
21
src/config/settings/development.py
Normal file
21
src/config/settings/development.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from .base import *
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / 'static'
|
||||||
|
]
|
20
src/config/settings/production.py
Normal file
20
src/config/settings/production.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from .base import *
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': env('DB_NAME'),
|
||||||
|
'USER': env('DB_USER'),
|
||||||
|
'PASSWORD': env('DB_PASSWORD'),
|
||||||
|
'HOST': 'databasepostgresql',
|
||||||
|
'PORT': '5432',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATIC_URL = '/django_static/'
|
||||||
|
STATIC_ROOT = os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'opt/services/djangoapp/django_static')
|
9
src/config/urls.py
Normal file
9
src/config/urls.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('account/', include('account.urls')),
|
||||||
|
path('', include('series.urls')),
|
||||||
|
]
|
16
src/config/wsgi.py
Normal file
16
src/config/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for config 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.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
22
src/manage.py
Executable file
22
src/manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/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', 'config.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()
|
0
src/series/__init__.py
Normal file
0
src/series/__init__.py
Normal file
5
src/series/admin.py
Normal file
5
src/series/admin.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import SeriesModel
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(SeriesModel)
|
6
src/series/apps.py
Normal file
6
src/series/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'series'
|
42
src/series/models.py
Normal file
42
src/series/models.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from API_client import APIClient
|
||||||
|
from django.core.validators import (MaxValueValidator,
|
||||||
|
MinValueValidator,
|
||||||
|
MinLengthValidator)
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesModel(models.Model):
|
||||||
|
user = models.ForeignKey('account.User', on_delete=models.CASCADE, related_name='series')
|
||||||
|
title = models.CharField(max_length=35, blank=False, null=False)
|
||||||
|
imdb_id = models.CharField(max_length=10, validators=[MinLengthValidator(9)],
|
||||||
|
blank=False, null=False)
|
||||||
|
last_season = models.IntegerField(validators=[
|
||||||
|
MaxValueValidator(30),
|
||||||
|
MinValueValidator(1)
|
||||||
|
], blank=False, null=False)
|
||||||
|
last_episode = models.IntegerField(validators=[
|
||||||
|
MaxValueValidator(60),
|
||||||
|
MinValueValidator(1)
|
||||||
|
], blank=False, null=False)
|
||||||
|
show = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def clean(self) -> None:
|
||||||
|
if len(self.imdb_id) < 9:
|
||||||
|
raise ValidationError(f'Ensure this value has at least 9 characters (it has {len(self.imdb_id)}).')
|
||||||
|
|
||||||
|
if len(self.imdb_id) > 10:
|
||||||
|
raise ValidationError(f'Make sure this value is no more than 10 characters (it has {len(self.imdb_id)}).')
|
||||||
|
|
||||||
|
client = APIClient(self.user.imdb_api_key)
|
||||||
|
|
||||||
|
seasons = client.get_seasons(self.imdb_id)
|
||||||
|
if str(self.last_season) not in seasons:
|
||||||
|
raise ValidationError('The season number you entered is not correct.')
|
||||||
|
|
||||||
|
episodes_count = client.get_episodes_count(self.last_season, self.imdb_id)
|
||||||
|
if self.last_episode > episodes_count:
|
||||||
|
raise ValidationError('The episode number you entered is not correct.')
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.user.username} - {self.title}"
|
9
src/series/templates/homepage.html
Normal file
9
src/series/templates/homepage.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %} Home Page {% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% include 'components/message.html' %}
|
||||||
|
|
||||||
|
{% endblock content %}
|
3
src/series/tests.py
Normal file
3
src/series/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
8
src/series/urls.py
Normal file
8
src/series/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from series import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.homepage_view, name='homepage'),
|
||||||
|
# path('series/create', views.homepage_view, name='homepage'),
|
||||||
|
]
|
1
src/series/views/__init__.py
Normal file
1
src/series/views/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .homepage import homepage_view
|
7
src/series/views/homepage.py
Normal file
7
src/series/views/homepage.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url='/account/login')
|
||||||
|
def homepage_view(request):
|
||||||
|
return render(request, 'homepage.html')
|
7
src/static/css/bootstrap.min.css
vendored
Normal file
7
src/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
35
src/static/css/footer.css
Normal file
35
src/static/css/footer.css
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 65px;
|
||||||
|
background: black;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-left {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-left: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-right {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer div span a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-links{
|
||||||
|
border: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-icons{
|
||||||
|
font-size: 23px;
|
||||||
|
}
|
BIN
src/static/icons/retro-tv.png
Normal file
BIN
src/static/icons/retro-tv.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
7
src/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
src/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
src/static/js/bootstrap.min.js
vendored
Normal file
7
src/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/static/js/popper.min.js
vendored
Normal file
6
src/static/js/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
30
src/templates/base.html
Normal file
30
src/templates/base.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<!-- neye bakıyon lan html var css var neye bakıyon -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||||
|
|
||||||
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include "components/navbar.html" %}
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
{% block content %}{% endblock content %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "components/footer.html" %}
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
24
src/templates/components/footer.html
Normal file
24
src/templates/components/footer.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{% static 'css/footer.css' %}">
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="div-left" style="float: left;">
|
||||||
|
<span class="text-muted">
|
||||||
|
<a href="mailto:ayxan.shahsuvarov1@gmail.com">© 2022 Aykhan Shahsuvarov</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="div-right" style="float: right;">
|
||||||
|
<a class="btn btn-outline-light btn-floating m-1 contact-links" target="_blank" href="https://github.com/Ayxan-z" role="button">
|
||||||
|
<i class="fab fa-github contact-icons"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-outline-light btn-floating m-1 contact-links" target="_blank" href="https://www.linkedin.com/in/ayxan-shahsuvarov-59a314187" role="button">
|
||||||
|
<i class="fab fa-linkedin-in contact-icons"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-outline-light btn-floating m-1 contact-links" target="_blank" href="mailto:ayxan.shahsuvarov1@gmail.com" role="button">
|
||||||
|
<i class="fa fa-envelope contact-icons"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
9
src/templates/components/message.html
Normal file
9
src/templates/components/message.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
{% if message.tags == "info" %}
|
||||||
|
<div class="alert alert-danger">{{ message }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
30
src/templates/components/navbar.html
Normal file
30
src/templates/components/navbar.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/" style="padding-top: 2px; padding-bottom: 2px;">
|
||||||
|
<img src="{% static 'icons/retro-tv.png' %}" width="55px" height="55px">
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="me-auto"></ul>
|
||||||
|
{% if request.user.is_authenticated == True %}
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" aria-current="page" href="{% url 'profile' %}" style="font-size: 18px;">
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" aria-current="page" href="{% url 'logout' %}" style="font-size: 18px;">
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
Loading…
x
Reference in New Issue
Block a user