mirror of
https://github.com/aykhans/series-robot-web.git
synced 2025-06-04 23:52:03 +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