Compare commits
No commits in common. "main" and "react-django" have entirely different histories.
main
...
react-djan
595 changed files with 7810 additions and 74116 deletions
|
@ -1,23 +0,0 @@
|
|||
.DS_Store
|
||||
._*
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
.github/
|
||||
|
||||
venv/
|
||||
.venv/
|
||||
.docker-venv/
|
||||
|
||||
build/
|
||||
dist/
|
||||
pip_dist/
|
||||
!pip_dist/archivebox.egg-info/requires.txt
|
||||
brew_dist/
|
||||
assets/
|
||||
|
||||
data/
|
||||
output/
|
||||
|
||||
tmp/
|
223
.gitignore
vendored
223
.gitignore
vendored
|
@ -1,3 +1,165 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@ -20,7 +182,6 @@ parts/
|
|||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
|
@ -50,6 +211,7 @@ coverage.xml
|
|||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -72,6 +234,7 @@ instance/
|
|||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
|
@ -82,7 +245,9 @@ profile_default/
|
|||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
|
@ -91,7 +256,30 @@ ipython_config.py
|
|||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
|
@ -128,9 +316,28 @@ dmypy.json
|
|||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Files and folder
|
||||
tmp/
|
||||
*~
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
.Cookies/
|
||||
archivist/media/
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# ValKey (Redis)
|
||||
dump.rdb
|
||||
|
||||
# Folders
|
||||
tmp/
|
||||
backend/media/
|
||||
|
|
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"artstation",
|
||||
"devantart",
|
||||
"iframes",
|
||||
"LANCZOS",
|
||||
"ncols",
|
||||
"nostatic",
|
||||
"phash",
|
||||
"Popover",
|
||||
"preimport",
|
||||
"runserver",
|
||||
"SLUGIFYING",
|
||||
"taggit",
|
||||
"viewsets",
|
||||
"virtualenv",
|
||||
"whitenoise"
|
||||
],
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
# Use an official Python runtime as a parent image
|
||||
#FROM python:3.10-slim-bullseye
|
||||
FROM python:3.11-alpine
|
||||
|
||||
LABEL name="Gallery-Archivist" \
|
||||
maintainer="Aroy-Art" \
|
||||
description="All-in-one personal social-media/art site archiving container" \
|
||||
homepage="https://git.aroy-art.com/Aroy/Gallery-Archivist" \
|
||||
documentation=""
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Install apk dependencies
|
||||
RUN apk update && \
|
||||
apk add curl rustup cargo make gcc g++ automake subversion python3-dev
|
||||
|
||||
# Create non-privileged user
|
||||
ARG USER_ID
|
||||
RUN adduser -D -u $USER_ID archivist
|
||||
#RUN addgroup -S $GROUP \
|
||||
# && adduser --system --create-home --gid $GROUP --groups audio,video $USER
|
||||
|
||||
# Download latest yt-dlp version and make executable
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp && \
|
||||
chmod a+rx /usr/local/bin/yt-dlp
|
||||
|
||||
# Install latest stable gallery-dl
|
||||
RUN pip install -U gallery-dl
|
||||
|
||||
COPY ./requirements.txt /app/
|
||||
|
||||
# Set the working directory to /app
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the current directory contents into the container at /app
|
||||
#COPY . /app
|
||||
|
||||
# Install any needed packages specified in requirements.txt
|
||||
#RUN export PYTHONPATH=/usr/bin/python && pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Make port 8000 available to the world outside this container
|
||||
EXPOSE 8000
|
||||
|
||||
USER archivist
|
||||
|
||||
# Define environment variable
|
||||
#ENV DJANGO_SETTINGS_MODULE=mysite.settings.production
|
||||
|
||||
# Run the command to start Django
|
||||
#CMD ["python", "archivist/manage.py", "runserver", "0.0.0.0:8000"]
|
203
README.md
203
README.md
|
@ -1,199 +1,8 @@
|
|||
# Gallery-Archivist
|
||||
# Gallery Archivist
|
||||
|
||||
---
|
||||
[](https://nogithub.codeberg.page)
|
||||
**Note:** This is an early prototype and is not intended for use in production.
|
||||
|
||||
My try to make a Social media archiving web tool with django and gallery-dl
|
||||
|
||||
## Features / Roadmap
|
||||
|
||||
*Note!* This is still in early development so stuff **will** change.
|
||||
|
||||
- [ ] Scraping sites primarily with gallery-dl, but also for other links found in posts.
|
||||
- [ ] Scheduled tasks
|
||||
- [ ] Site support
|
||||
- [ ] [Furaffinity](https://www.furaffinity.net)
|
||||
- [ ] [Twitter/X](https://twitter.com/)
|
||||
- [x] User import With profile/banner images
|
||||
- [ ] Import images from timeline
|
||||
- [ ] Media support and previews
|
||||
- [x] Flash (With [Ruffle](https://github.com/ruffle-rs/ruffle/))
|
||||
- [x] PDF (With HTML embed tag)
|
||||
- [x] Image (With HTML img tag)
|
||||
- [ ] Video (With [FluidPlayer](https://www.fluidplayer.com/))
|
||||
- [ ] Compressed archive previews
|
||||
- [ ] other text documents
|
||||
- [ ] Easy download list of all files of post and user
|
||||
|
||||
---
|
||||
|
||||
## Trademarks
|
||||
|
||||
### External Sites
|
||||
|
||||
The logos of external sites used in Gallery-Archivist are trademarks of their respective owners. The use of these trademarks does not indicate endorsement of the trademark holder by the repository, its owners or contributors.
|
||||
Gallery-Archivist is not endorsed by or affiliated with any of the trademark holders.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Run without Docker
|
||||
|
||||
- **Step 1:** Clone repo
|
||||
|
||||
```bash
|
||||
git clone https://git.aroy-art.com/Aroy/Gallery-Archivist.git
|
||||
```
|
||||
|
||||
- **Step 2:** Change dir to the new cloned repo
|
||||
|
||||
```bash
|
||||
cd Gallery-Archivist
|
||||
```
|
||||
|
||||
- **Step 3:** Make a python environment
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
```
|
||||
|
||||
- **Step 4:** Activate the new python environment
|
||||
|
||||
```bash
|
||||
source ./venv/bin/activate
|
||||
```
|
||||
|
||||
- **Step 5:** Install python dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- **Step 6:** Copy environment config file and change it to your liking
|
||||
|
||||
```bash
|
||||
cp env-sample .env
|
||||
```
|
||||
|
||||
- **Step 7:** Change directory to `./archivist`
|
||||
|
||||
```bash
|
||||
cd ./archivist
|
||||
```
|
||||
|
||||
- **Step 8:** Run django server
|
||||
|
||||
```bash
|
||||
python3 manage.py runserver
|
||||
```
|
||||
|
||||
### Manual Import Data
|
||||
|
||||
It is possible to import data from saved gallery-dl json files
|
||||
|
||||
- **Step 1:** Make sure that you are in the root folder of the project.
|
||||
|
||||
- **Step 2:** Make sure to activate the python environment.
|
||||
|
||||
```bash
|
||||
source ./venv/bin/activate
|
||||
```
|
||||
|
||||
- **Step 3:** Change directory to `./archivist`
|
||||
|
||||
```bash
|
||||
cd ./archivist
|
||||
```
|
||||
|
||||
- **Step 4:** Import the json data and media file from gallery-dl
|
||||
replace `<path>` with the path of the folder or json file to import
|
||||
|
||||
```bash
|
||||
python manage.py import_data <path>
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Without Docker
|
||||
|
||||
*Note!* Instructions are made for a modern linux environment.
|
||||
|
||||
- **Step 0:** Make sure that you have installed the dependencies.
|
||||
You need `git, python, python-virtualenv, redis`
|
||||
|
||||
- **Step 1:** Clone repo
|
||||
|
||||
```bash
|
||||
git clone https://git.aroy-art.com/Aroy/Gallery-Archivist.git
|
||||
```
|
||||
|
||||
- **Step 2:** Change dir to the new cloned repo
|
||||
|
||||
```bash
|
||||
cd Gallery-Archivist
|
||||
```
|
||||
|
||||
- **Step 3:** Make a python environment
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
```
|
||||
|
||||
- **Step 4:** Activate the new python environment
|
||||
|
||||
```bash
|
||||
source ./venv/bin/activate
|
||||
```
|
||||
|
||||
- **Step 5:** Install python dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- **Step 6:** Copy environment config file and change it to your liking
|
||||
|
||||
```bash
|
||||
cp env-sample archivist/.env
|
||||
```
|
||||
|
||||
- **Step 7:** Change directory to archivist
|
||||
|
||||
```bash
|
||||
cd archivist
|
||||
```
|
||||
|
||||
- **Step 8:** Crate and Run django database migrations
|
||||
|
||||
```Bash
|
||||
python manage.py makemigrations
|
||||
```
|
||||
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
- **Step 9:** Start the redis server
|
||||
|
||||
```bash
|
||||
redis-server
|
||||
```
|
||||
|
||||
- **Step :10** Open another shell inside of `archivist/` folder and start the django server
|
||||
|
||||
```bash
|
||||
python3 manage.py runserver
|
||||
```
|
||||
|
||||
- **Step :11** Open another shell inside of `archivist/` folder and start the celery worker
|
||||
|
||||
```bash
|
||||
celery -A core worker -l info
|
||||
```
|
||||
|
||||
- **Step :12** Open another shell inside of `archivist/` folder and start the celery beat
|
||||
|
||||
```bash
|
||||
celery -A core beat -l info
|
||||
```
|
||||
This is a complete rebuild of the [Gallery Archivist](https://git.aroy-art.com/Aroy/Gallery-Archivist) project.
|
||||
With a new frontend built with React and Vite and also a
|
||||
complete restructure of the django backend to only serve
|
||||
the API and database.
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class APIConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.api'
|
||||
label = 'apps_api'
|
|
@ -1,22 +0,0 @@
|
|||
# permissions.py
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
def check_admin(user):
|
||||
"""check for admin permission for restricted views"""
|
||||
return user.is_staff or user.groups.filter(name="admin").exists()
|
||||
|
||||
|
||||
class AdminOnly(permissions.BasePermission):
|
||||
"""allow only admin"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return check_admin(request.user)
|
||||
|
||||
|
||||
class AdminOnlyOrReadOnly(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
return check_admin(request.user)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
urlpatterns = [
|
||||
path('profile/seen_posts/<str:submission_hash>', UserProfileAPIView.seen_post, name='user_seen_post'),
|
||||
]
|
|
@ -1,42 +0,0 @@
|
|||
from django.http import JsonResponse
|
||||
|
||||
# Create your views here.
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
|
||||
from apps.user.models import UserProfile, SeenPost
|
||||
|
||||
from apps.sites.models import Submissions
|
||||
|
||||
class UserProfileAPIView(APIView):
|
||||
|
||||
def seen_post(request, submission_hash):
|
||||
|
||||
user = UserProfile.objects.get(user=request.user)
|
||||
|
||||
if request.method == 'GET':
|
||||
try:
|
||||
submission = Submissions.objects.get(submission_hash=submission_hash)
|
||||
|
||||
try:
|
||||
SeenPost.objects.get(user=user, post_id=submission.pk)
|
||||
|
||||
return JsonResponse({'seen': True}, status=status.HTTP_200_OK)
|
||||
|
||||
except SeenPost.DoesNotExist:
|
||||
return JsonResponse({'seen': False}, status=status.HTTP_200_OK)
|
||||
|
||||
except Submissions.DoesNotExist:
|
||||
return JsonResponse({'message': 'Submission not found.'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
if request.method == 'PUT':
|
||||
submission = Submissions.objects.get(submission_hash=submission_hash)
|
||||
|
||||
SeenPost.objects.get_or_create(user=user, post_id=submission.pk)
|
||||
|
||||
return JsonResponse({'seen': True}, status=status.HTTP_200_OK)
|
||||
|
||||
else:
|
||||
return JsonResponse({'message': 'Only GET and PUT requests are allowed.'}, status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthConfig(AppConfig):
|
||||
name = 'apps.auth'
|
||||
label = 'apps_auth'
|
|
@ -1,55 +0,0 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
username = forms.CharField(
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"placeholder": "Username",
|
||||
"class": "form-control"
|
||||
}
|
||||
))
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"placeholder": "Password",
|
||||
"class": "form-control"
|
||||
}
|
||||
))
|
||||
|
||||
|
||||
class SignUpForm(UserCreationForm):
|
||||
username = forms.CharField(
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"placeholder": "Username",
|
||||
"class": "form-control"
|
||||
}
|
||||
))
|
||||
email = forms.EmailField(
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
"placeholder": "Email",
|
||||
"class": "form-control"
|
||||
}
|
||||
))
|
||||
password1 = forms.CharField(
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"placeholder": "Password",
|
||||
"class": "form-control"
|
||||
}
|
||||
))
|
||||
password2 = forms.CharField(
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"placeholder": "Password check",
|
||||
"class": "form-control"
|
||||
}
|
||||
))
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'email', 'password1', 'password2')
|
|
@ -1,9 +0,0 @@
|
|||
from django.urls import path
|
||||
from .views import login_view, register_user
|
||||
from django.contrib.auth.views import LogoutView
|
||||
|
||||
urlpatterns = [
|
||||
path("login/", login_view, name="login"),
|
||||
path("register/", register_user, name="register"),
|
||||
path("logout/", LogoutView.as_view(), name="logout"),
|
||||
]
|
|
@ -1,68 +0,0 @@
|
|||
# Create your views here.
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import authenticate, login
|
||||
|
||||
from apps.user.models import UserProfile # Importing the UserProfile model
|
||||
|
||||
from .forms import LoginForm, SignUpForm # Importing form classes for login and signup
|
||||
|
||||
|
||||
# View function for user login
|
||||
def login_view(request):
|
||||
# Check if the user is already authenticated, if so, redirect them to the home page
|
||||
if request.user.is_authenticated:
|
||||
next_page = request.GET.get('next', '/') # Get the 'next' parameter from the URL, default to '/'
|
||||
return redirect(next_page)
|
||||
|
||||
form = LoginForm(request.POST or None) # Create a login form instance
|
||||
|
||||
msg = None # Initialize a message variable
|
||||
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
username = form.cleaned_data.get("username")
|
||||
password = form.cleaned_data.get("password")
|
||||
|
||||
# Authenticate user using the provided username and password
|
||||
user = authenticate(username=username, password=password)
|
||||
|
||||
if user is not None:
|
||||
login(request, user) # Log in the authenticated user
|
||||
next_page = request.GET.get('next', '/') # Get the 'next' parameter from the URL
|
||||
return redirect(next_page) # Redirect to the 'next' page after successful login
|
||||
else:
|
||||
msg = 'Invalid credentials' # Set error message for invalid credentials
|
||||
else:
|
||||
msg = 'Error validating the form' # Set error message for form validation error
|
||||
|
||||
return render(request, "accounts/login.html", {"form": form, "msg": msg})
|
||||
|
||||
|
||||
# View function for user registration
|
||||
def register_user(request):
|
||||
msg = None # Initialize a message variable
|
||||
success = False # Initialize a success flag
|
||||
|
||||
if request.method == "POST":
|
||||
form = SignUpForm(request.POST) # Create a signup form instance
|
||||
|
||||
if form.is_valid():
|
||||
form.save() # Save the user details from the form
|
||||
username = form.cleaned_data.get("username")
|
||||
raw_password = form.cleaned_data.get("password1")
|
||||
|
||||
# Authenticate the newly registered user
|
||||
user = authenticate(username=username, password=raw_password)
|
||||
|
||||
# Create a UserProfile instance associated with the registered user
|
||||
profile = UserProfile(user=user)
|
||||
profile.save()
|
||||
|
||||
msg = 'User created - please <a href="/login">login</a>.' # Set success message with a login link
|
||||
success = True # Set success flag to True
|
||||
else:
|
||||
msg = 'Form is not valid' # Set error message for invalid form data
|
||||
else:
|
||||
form = SignUpForm() # Create an empty signup form instance for GET requests
|
||||
|
||||
return render(request, "accounts/register.html", {"form": form, "msg": msg, "success": success})
|
|
@ -1,4 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
def debug(context):
|
||||
return {'DEBUG': settings.DEBUG}
|
|
@ -1,63 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
from .models import Metadata_Files, Submission_File, User_Profile_Images, User_Banner_Images
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class Submission_FileAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'file_link', 'date_added', 'file_hash', 'file',)
|
||||
search_fields = ['file_name', 'file_hash']
|
||||
|
||||
def file_link(self, obj):
|
||||
url = reverse("admin:files_submission_file_change", args=[obj.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.file_name)
|
||||
|
||||
file_link.short_description = 'File Name'
|
||||
|
||||
|
||||
class User_Banner_ImagesAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'file_link', 'date_added', 'file_hash', 'file',)
|
||||
|
||||
def file_link(self, obj):
|
||||
url = reverse("admin:files_user_banner_images_change", args=[obj.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.file_name)
|
||||
|
||||
file_link.short_description = 'File Name'
|
||||
|
||||
|
||||
class User_Profile_ImagesAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'file_link', 'date_added', 'file_hash', 'file',)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
form.base_fields['file_hash'].widget = forms.TextInput(attrs={'readonly': True})
|
||||
return form
|
||||
|
||||
def file_link(self, obj):
|
||||
url = reverse("admin:files_user_profile_images_change", args=[obj.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.file_name)
|
||||
|
||||
file_link.short_description = 'File Name'
|
||||
|
||||
class Metadata_FilesAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'file_link', 'date_added', 'file_hash', 'file',)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
form.base_fields['file_hash'].widget = forms.TextInput(attrs={'readonly': True})
|
||||
return form
|
||||
|
||||
def file_link(self, obj):
|
||||
url = reverse("admin:files_metadata_files_change", args=[obj.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.file_name)
|
||||
|
||||
file_link.short_description = 'File Name'
|
||||
|
||||
|
||||
admin.site.register(Metadata_Files, Metadata_FilesAdmin)
|
||||
admin.site.register(Submission_File, Submission_FileAdmin)
|
||||
admin.site.register(User_Banner_Images, User_Banner_ImagesAdmin)
|
||||
admin.site.register(User_Profile_Images, User_Profile_ImagesAdmin)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FilesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.files'
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FilesConfig(AppConfig):
|
||||
name = 'apps.files'
|
||||
label = 'apps_files'
|
|
@ -1,7 +0,0 @@
|
|||
from django import forms
|
||||
from .models import Submission_File
|
||||
|
||||
class UploadFileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Submission_File
|
||||
fields = ['file']
|
|
@ -1,111 +0,0 @@
|
|||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Create your models here.
|
||||
|
||||
def get_upload_to(instance, filename, folder):
|
||||
return f'{folder}/{instance.file_hash[:2]}/{filename}'
|
||||
|
||||
def get_upload_to_metadata(instance, filename):
|
||||
return get_upload_to(instance, filename, 'metadata')
|
||||
|
||||
def get_upload_to_submission(instance, filename):
|
||||
return get_upload_to(instance, filename, 'submissions')
|
||||
|
||||
def get_upload_to_profile(instance, filename):
|
||||
return get_upload_to(instance, filename, 'profiles')
|
||||
|
||||
def get_upload_to_banner(instance, filename):
|
||||
return get_upload_to(instance, filename, 'banners')
|
||||
|
||||
|
||||
class User_Profile_Images(models.Model):
|
||||
|
||||
file_hash = models.CharField(unique=True, max_length=64)
|
||||
file_name = models.CharField(max_length=150, blank=True)
|
||||
file = models.FileField(upload_to=get_upload_to_profile, blank=True)
|
||||
file_mime = models.CharField(max_length=64, blank=True)
|
||||
file_ext = models.CharField(max_length=64, blank=True)
|
||||
size = models.PositiveIntegerField(null=True)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User Profile Image")
|
||||
verbose_name_plural = _("User Profile Images")
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('files:serve_content_file', args=['user_profile', self.file_hash])
|
||||
|
||||
|
||||
class User_Banner_Images(models.Model):
|
||||
|
||||
file_hash = models.CharField(unique=True, max_length=64)
|
||||
file_name = models.CharField(max_length=150, blank=True)
|
||||
file = models.FileField(upload_to=get_upload_to_banner, blank=True)
|
||||
file_mime = models.CharField(max_length=64, blank=True)
|
||||
file_ext = models.CharField(max_length=64, blank=True)
|
||||
size = models.PositiveIntegerField(null=True)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User Banner Image")
|
||||
verbose_name_plural = _("User Banner Images")
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('files:serve_content_file', args=['user_banner', self.file_hash])
|
||||
|
||||
|
||||
class Submission_File(models.Model):
|
||||
|
||||
file_hash = models.CharField(unique=True, max_length=64)
|
||||
file_name = models.CharField(max_length=150, blank=True)
|
||||
file = models.FileField(upload_to=get_upload_to_submission, blank=True)
|
||||
file_mime = models.CharField(max_length=64, blank=True)
|
||||
file_ext = models.CharField(max_length=64, blank=True)
|
||||
size = models.PositiveIntegerField(null=True)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
|
||||
extra_file = models.BooleanField(default=False)
|
||||
file_source = models.CharField(max_length=32, blank=True)
|
||||
|
||||
image_height = models.PositiveIntegerField(null=True)
|
||||
image_width = models.PositiveIntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Submission File")
|
||||
verbose_name_plural = _("Submission Files")
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('files:serve_content_file', args=['submission', self.file_hash])
|
||||
|
||||
|
||||
class Metadata_Files(models.Model):
|
||||
|
||||
file_hash = models.CharField(unique=True, max_length=64)
|
||||
file_name = models.CharField(max_length=150, blank=True)
|
||||
file = models.FileField(upload_to=get_upload_to_metadata, blank=True)
|
||||
file_mime = models.CharField(max_length=64, blank=True)
|
||||
file_ext = models.CharField(max_length=64, blank=True)
|
||||
size = models.PositiveIntegerField(null=True)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Metadata File")
|
||||
verbose_name_plural = _("Metadata Files")
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('files:serve_content_file', args=['metadata', self.file_hash])
|
|
@ -1,39 +0,0 @@
|
|||
import os
|
||||
import zipfile
|
||||
import json
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task
|
||||
def list_zip_contents(zip_path):
|
||||
"""List the contents of a ZIP file.
|
||||
|
||||
Args:
|
||||
zip_path (str): The path to the ZIP file.
|
||||
|
||||
Returns:
|
||||
str: A JSON string containing the list of files in the ZIP archive
|
||||
or an error/warning message if applicable.
|
||||
"""
|
||||
try:
|
||||
# Check if the ZIP file size exceeds 2 GiB
|
||||
if os.path.getsize(zip_path) >= 2 * 1024 * 1024 * 1024:
|
||||
return json.dumps({"warning": "archive file is too big (>2GiB), ignoring"})
|
||||
else:
|
||||
# Open the ZIP file
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
file_list = []
|
||||
# Iterate over each file in the ZIP archive
|
||||
for info in zip_ref.infolist():
|
||||
# Append file details to the list
|
||||
file_list.append({
|
||||
"name": info.filename,
|
||||
"size": info.file_size,
|
||||
"date": info.date_time,
|
||||
"crc": info.CRC,
|
||||
"compressed_size": info.compress_size,
|
||||
})
|
||||
# Return the list of files as a JSON string
|
||||
return json.dumps({"files": file_list})
|
||||
except Exception as e:
|
||||
# Return an error message if an exception occurs
|
||||
return json.dumps({"error": str(e)})
|
|
@ -1,11 +0,0 @@
|
|||
from django.urls import re_path, path
|
||||
from .views import serve_content_file, fileUpload
|
||||
|
||||
app_name = "files"
|
||||
|
||||
urlpatterns = [
|
||||
# Add a URL pattern that captures the file path
|
||||
path('<folder>/<str:file_hash>', serve_content_file, name='serve_content_file'),
|
||||
# Other URL patterns if any
|
||||
path('upload/', fileUpload, name='file_upload'),
|
||||
]
|
|
@ -1,105 +0,0 @@
|
|||
# Create your views here.
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import FileResponse, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
import os
|
||||
from blake3 import blake3
|
||||
|
||||
from .forms import UploadFileForm
|
||||
from .models import User_Banner_Images, User_Profile_Images, Metadata_Files, Submission_File
|
||||
|
||||
|
||||
MODEL_MAP = {
|
||||
'user_profile': User_Profile_Images,
|
||||
'user_banner': User_Banner_Images,
|
||||
'submission': Submission_File,
|
||||
'metadata': Metadata_Files,
|
||||
}
|
||||
|
||||
def compute_file_hash(file):
|
||||
'''
|
||||
Compute BLAKE3 hash of the file
|
||||
'''
|
||||
try:
|
||||
hasher = blake3()
|
||||
for chunk in file.chunks(chunk_size=65536):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
except Exception as e:
|
||||
print(f"Error computing file hash: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def serve_content_file(request, folder, file_hash):
|
||||
'''
|
||||
View function to serve content files for download or inline viewing
|
||||
'''
|
||||
|
||||
ModelClass = MODEL_MAP.get(folder)
|
||||
if ModelClass is None:
|
||||
return HttpResponse("Invalid folder", status=404)
|
||||
|
||||
download = request.GET.get('d')
|
||||
|
||||
try:
|
||||
obj_file = get_object_or_404(ModelClass, file_hash=file_hash)
|
||||
file = obj_file.file.file
|
||||
file_name = obj_file.file_name
|
||||
|
||||
response = FileResponse(file)
|
||||
if download == "1":
|
||||
response['Content-Disposition'] = f'attachment; filename="{file_name}"'
|
||||
else:
|
||||
response['Content-Disposition'] = f'inline; filename="{file_name}"'
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error serving file: {e}")
|
||||
return HttpResponse("File not found", status=404)
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def fileUpload(request):
|
||||
'''
|
||||
View function for handling file uploads
|
||||
'''
|
||||
if request.method == 'POST':
|
||||
form = UploadFileForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
if 'file' in request.FILES: # Check if a file has been uploaded
|
||||
file = form.cleaned_data['file']
|
||||
|
||||
file_name = file.name
|
||||
file_hash = compute_file_hash(file)
|
||||
|
||||
Null, file_ext = os.path.splitext(file_name)
|
||||
hash_file_name = file_hash + file_ext
|
||||
|
||||
new_submission_file, created = Submission_File.objects.get_or_create(file_hash=file_hash)
|
||||
|
||||
new_submission_file.file_hash = file_hash
|
||||
new_submission_file.file_name = file_name
|
||||
new_submission_file.file.save(hash_file_name, file)
|
||||
|
||||
new_submission_file.save
|
||||
|
||||
return HttpResponseRedirect(f"/files/submission/{file_hash}")
|
||||
|
||||
else:
|
||||
# No file was uploaded, add an error message to the context
|
||||
error_message = 'No file was uploaded.'
|
||||
return render(request, 'files/upload.html', {'form': form, 'error_message': error_message})
|
||||
|
||||
else:
|
||||
# Form is not valid, add an error message to the context
|
||||
error_message = 'There was an error with the form.'
|
||||
return render(request, 'files/upload.html', {'form': form, 'error_message': error_message})
|
||||
|
||||
else:
|
||||
form = UploadFileForm()
|
||||
|
||||
return render(request, 'files/upload.html', {'form': form})
|
|
@ -1,9 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import ImportSourceURLs
|
||||
|
||||
|
||||
class ImportSourceURLsAdmin(admin.ModelAdmin):
|
||||
list_display = ('url', 'added_by_user', 'last_imported', 'source_type', 'category', 'date_added')
|
||||
|
||||
admin.site.register(ImportSourceURLs, ImportSourceURLsAdmin)
|
|
@ -1,7 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ImporterConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.importer'
|
||||
label = 'apps.importer'
|
|
@ -1,22 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
from .models import ImportSourceURLs
|
||||
|
||||
|
||||
class ImportSourceURLsForm(forms.Form):
|
||||
url = forms.URLField(
|
||||
label="Add URL",
|
||||
required=True,
|
||||
widget=forms.TextInput(
|
||||
attrs={"placeholder": "https://example.com", "class": "form-control"}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GalleryDLConfigForm(forms.Form):
|
||||
text = forms.CharField(
|
||||
label="GalleryDL Config",
|
||||
required=True,
|
||||
widget=forms.Textarea(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.sites.models import Category
|
||||
|
||||
from apps.user.models import UserProfile
|
||||
|
||||
class ImportSourceURLs(models.Model):
|
||||
|
||||
SOURCE_TYPES = (
|
||||
('C', 'Complete User'),
|
||||
('P', 'Singel Post'),
|
||||
)
|
||||
|
||||
url = models.URLField(unique=True)
|
||||
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||
|
||||
added_by_user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
|
||||
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
|
||||
last_imported = models.DateTimeField(editable=True, blank=True, null=True)
|
||||
|
||||
source_type = models.CharField(max_length=1, choices=SOURCE_TYPES, default=None)
|
||||
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Import Source URL")
|
||||
verbose_name_plural = _("Import Source URLs")
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
|
@ -1,12 +0,0 @@
|
|||
import time
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
@shared_task
|
||||
def wait(x):
|
||||
time.sleep(x)
|
||||
return f"Sleeping for {x} seconds"
|
|
@ -1,59 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.urls import reverse
|
||||
|
||||
from django_celery_results.models import TaskResult
|
||||
|
||||
from .models import ImportSourceURLs
|
||||
from .forms import ImportSourceURLsForm, GalleryDLConfigForm
|
||||
|
||||
from apps.sites.models import Category
|
||||
|
||||
from core import settings
|
||||
|
||||
|
||||
NAVTABS = [
|
||||
{
|
||||
"name" : "Home",
|
||||
"url" : "importer:index",
|
||||
"adminOnly" : False
|
||||
},
|
||||
{
|
||||
"name" : "Source URLs",
|
||||
"url" : "importer:source_urls",
|
||||
"adminOnly" : False
|
||||
},
|
||||
{
|
||||
"name" : "Config",
|
||||
"url" : "importer:config",
|
||||
"adminOnly" : True
|
||||
},
|
||||
{
|
||||
"name" : "Tasks",
|
||||
"url" : "importer:tasks",
|
||||
"adminOnly" : True
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@login_required(login_url="login")
|
||||
def TasksView(request):
|
||||
|
||||
if not (request.user.is_staff or request.user.is_superuser):
|
||||
return redirect('importer:index')
|
||||
|
||||
|
||||
from django_celery_results.models import TaskResult
|
||||
|
||||
tasks = TaskResult.objects.all().order_by('-date_created')
|
||||
|
||||
context = {
|
||||
"tasks" : tasks,
|
||||
"tabs" : NAVTABS
|
||||
}
|
||||
|
||||
return render(request, context=context, template_name='importer/tasks.html')
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SitesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.sites'
|
|
@ -1,7 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SitesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.sites'
|
||||
label = 'apps.sites'
|
|
@ -1,66 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
from .models import Category
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
q = forms.CharField(
|
||||
label='Search',
|
||||
max_length=100,
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Search'
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
category = forms.ModelChoiceField(
|
||||
label="Site",
|
||||
queryset=Category.objects.all(),
|
||||
empty_label="All Sites", # Sets the name of the null option
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
'class': 'form-select',
|
||||
'placeholder': 'All Sites'
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
sort = forms.ChoiceField(
|
||||
label="Sort by",
|
||||
choices=[('1', 'Date'), ('2', 'Views'), ('3', 'Likes'), ('4', 'Relevance')],
|
||||
initial='1',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
'class': 'form-select',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
sort_order = forms.ChoiceField(
|
||||
label="Sort order",
|
||||
choices=[('1', 'Descending'), ('2', 'Ascending')],
|
||||
initial='1',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
'class': 'form-select',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
mature = forms.ChoiceField(
|
||||
label="Filter by Mature",
|
||||
choices=[('1', 'All'),('2', 'General'), ('3', 'Mature/Adult')],
|
||||
initial='1',
|
||||
required=False,
|
||||
widget=forms.RadioSelect(
|
||||
attrs={
|
||||
'class': 'form-check-input',
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
@ -1,559 +0,0 @@
|
|||
# /management/commands/import_data.py
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from blake3 import blake3
|
||||
from tqdm.auto import tqdm
|
||||
from PIL import Image
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.text import slugify
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
|
||||
from apps.files.models import User_Profile_Images, User_Banner_Images, Submission_File, Metadata_Files
|
||||
|
||||
from apps.sites.models import Category, Submissions, Users, Tags
|
||||
|
||||
from apps.sites.furaffinity.models import FA_Submission, FA_Tags, FA_User, FA_Species, FA_Gender, FA_Mature
|
||||
from apps.sites.twitter.models import Twitter_Submissions, Twitter_Users, Twitter_Tags
|
||||
|
||||
from utils.files import get_mime_type
|
||||
from utils.strings import get_urls
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Import data from JSON files in a folder or a single JSON file to the Twitter archive'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('path', type=str, help='Path to the folder containing JSON files or a single JSON file')
|
||||
parser.add_argument('--delete', action='store_true', help='Delete imported files')
|
||||
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
path = kwargs['path']
|
||||
delete = kwargs['delete']
|
||||
|
||||
if os.path.isfile(path):
|
||||
self.process_json_file(path, delete)
|
||||
elif os.path.isdir(path):
|
||||
self.process_json_folder(path, delete)
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f"The path '{path}' is not a valid file or folder."))
|
||||
return
|
||||
|
||||
|
||||
def process_json_file(self, file_path, delete):
|
||||
#self.stdout.write(self.style.NOTICE(f"Importing data from: {file_path}"))
|
||||
tqdm.write(f"Importing data from: {file_path}")
|
||||
|
||||
with open(file_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.import_data(data, file_path, delete)
|
||||
|
||||
tqdm.write(self.style.SUCCESS('Data imported successfully.'))
|
||||
|
||||
|
||||
def process_json_folder(self, folder_path, delete):
|
||||
if not os.path.exists(folder_path):
|
||||
#self.stdout.write(self.style.ERROR(f"The folder '{folder_path}' does not exist."))
|
||||
tqdm.write(self.style.ERROR(f"The folder '{folder_path}' does not exist."))
|
||||
return
|
||||
|
||||
for root, dirs, files in tqdm(os.walk(folder_path), dynamic_ncols=True):
|
||||
for file_name in files:
|
||||
if file_name.endswith('.json'):
|
||||
file_path = os.path.join(root, file_name)
|
||||
self.process_json_file(file_path, delete)
|
||||
|
||||
|
||||
def compute_file_hash(self, file_path):
|
||||
""" Compute BLAKE3 hash of the file """
|
||||
try:
|
||||
hasher = blake3()
|
||||
with open(file_path, 'rb') as f:
|
||||
while chunk := f.read(65536):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
except Exception as e:
|
||||
tqdm.write(self.style.WARNING(f"Error computing file hash: {e}"))
|
||||
return None
|
||||
def compute_string_hash(self, string):
|
||||
""" Compute BLAKE3 hash of the string """
|
||||
try:
|
||||
hasher = blake3()
|
||||
hasher.update(string.encode())
|
||||
return hasher.hexdigest()
|
||||
except Exception as e:
|
||||
tqdm.write(self.style.WARNING(f"Error computing string hash: {e}"))
|
||||
return None
|
||||
def import_file(self, file_path, model, delete=False):
|
||||
"""
|
||||
Imports a file if it doesn't already exist in the database and returns the instance.
|
||||
|
||||
:param file_path: The path to the file to import.
|
||||
:param model: The model class to which the file instance should be linked.
|
||||
:param delete: Whether to delete the imported file after processing.
|
||||
:return: The file instance.
|
||||
"""
|
||||
|
||||
file_instance = None # Initialize file_instance to None
|
||||
|
||||
if os.path.exists(file_path):
|
||||
file_hash = self.compute_file_hash(file_path)
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
Null, file_ext = os.path.splitext(file_name)
|
||||
hash_file_name = file_hash + file_ext
|
||||
|
||||
try:
|
||||
file_instance = model.objects.get(file_hash=file_hash)
|
||||
|
||||
file_instance.file_ext = file_ext
|
||||
file_instance.size = os.path.getsize(file_path)
|
||||
file_instance.file_mime = get_mime_type(file_path)
|
||||
|
||||
if file_instance.file_mime.startswith("image/"):
|
||||
im = Image.open(file_instance.file)
|
||||
file_instance.image_height, file_instance.image_width = im.size
|
||||
else:
|
||||
file_instance.image_height = None
|
||||
file_instance.image_width = None
|
||||
|
||||
file_instance.save()
|
||||
|
||||
tqdm.write(self.style.NOTICE(f"Skipping: {file_path} file, already imported"))
|
||||
|
||||
except model.DoesNotExist:
|
||||
# If the file doesn't exist, create a new file instance
|
||||
with open(file_path, 'rb') as file:
|
||||
file_instance = model()
|
||||
file_instance.file_hash = file_hash
|
||||
|
||||
file_instance.file.save(hash_file_name, file)
|
||||
|
||||
file_instance.file_ext = file_ext
|
||||
file_instance.file_mime = get_mime_type(file_path)
|
||||
file_instance.size = os.path.getsize(file_path)
|
||||
|
||||
if file_instance.file_mime.startswith("image/"):
|
||||
im = Image.open(file_instance.file)
|
||||
file_instance.image_height, file_instance.image_width = im.size
|
||||
else:
|
||||
file_instance.image_height = None
|
||||
file_instance.image_width = None
|
||||
|
||||
file_instance.file_name = file_name
|
||||
file_instance.save()
|
||||
|
||||
tqdm.write(self.style.NOTICE(f"Import file: {file_path}"))
|
||||
|
||||
if delete:
|
||||
self.delete_imported_file(file_path)
|
||||
|
||||
return file_instance
|
||||
|
||||
|
||||
def delete_imported_file(self, file_path, delete=False):
|
||||
"""
|
||||
Delete the file if the --delete flag is used
|
||||
|
||||
:param delete: Whether to delete the imported file after processing.
|
||||
"""
|
||||
if delete:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
tqdm.write(self.style.SUCCESS(f"Deleted: {file_path}"))
|
||||
else:
|
||||
tqdm.write(self.style.WARNING(f"File not found: {file_path}"))
|
||||
|
||||
|
||||
def import_data(self, data, json_file_path, delete):
|
||||
|
||||
category = data['category']
|
||||
|
||||
if category == "twitter":
|
||||
self.import_from_twitter(data, json_file_path, delete)
|
||||
|
||||
elif category == "furaffinity":
|
||||
self.import_from_furaffinity(data, json_file_path, delete)
|
||||
|
||||
else:
|
||||
tqdm.write(f"Skipping '{category}' not implemented")
|
||||
|
||||
def import_twitter_user(self, data, file_path, category, delete=False):
|
||||
"""
|
||||
Import a Twitter user from the provided data into the database.
|
||||
|
||||
Parameters:
|
||||
data (dict): The data containing information about the Twitter user.
|
||||
file_path (str): The file path for importing user images.
|
||||
delete (bool): Flag indicating whether to delete user images after importing it.
|
||||
|
||||
Returns:
|
||||
Twitter_Users: The Twitter user object imported or retrieved from the database.
|
||||
"""
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Twitter_Users)
|
||||
|
||||
author, created = Twitter_Users.objects.get_or_create(artist_id=data['author']['id'])
|
||||
|
||||
author.artist = data['author']['nick']
|
||||
author.artist_url = data['author']['name']
|
||||
author.date = timezone.make_aware(datetime.strptime(data['author']["date"], "%Y-%m-%d %H:%M:%S"))
|
||||
author.description = data['author']['description']
|
||||
if 'url' in data['author'].keys():
|
||||
author.extra_url = data['author']['url']
|
||||
author.location = data['author']['location']
|
||||
author.verified = data['author']['verified']
|
||||
|
||||
if author.favourites_count == None or data['author']["favourites_count"] > author.favourites_count:
|
||||
author.favourites_count = data['author']["favourites_count"]
|
||||
if author.followers_count == None or data['author']["followers_count"] > author.followers_count:
|
||||
author.followers_count = data['author']["followers_count"]
|
||||
if author.friends_count == None or data['author']["friends_count"] > author.friends_count:
|
||||
author.friends_count = data['author']["friends_count"]
|
||||
if author.media_count == None or data['author']["media_count"] > author.media_count:
|
||||
author.media_count = data['author']["media_count"]
|
||||
if author.listed_count == None or data['author']["listed_count"] > author.listed_count:
|
||||
author.listed_count = data['author']["listed_count"]
|
||||
if author.statuses_count == None or data['author']["statuses_count"] > author.statuses_count:
|
||||
author.statuses_count = data['author']["statuses_count"]
|
||||
|
||||
if data['subcategory'] == "avatar":
|
||||
author.profile_image = data['author']['profile_image']
|
||||
|
||||
author.icon = self.import_file(file_path, User_Profile_Images, delete)
|
||||
|
||||
elif data['subcategory'] == "background":
|
||||
|
||||
author.profile_banner = data['author']['profile_banner']
|
||||
|
||||
author.banner = self.import_file(file_path, User_Banner_Images, delete)
|
||||
|
||||
|
||||
author_hash = self.compute_string_hash(data['author']['name'] + data['category'])
|
||||
|
||||
site_user, created = Users.objects.get_or_create(user_hash=author_hash)
|
||||
|
||||
site_user.category = category
|
||||
|
||||
|
||||
# Get the primary key of the twitter_submission instance
|
||||
site_user_id = author.pk
|
||||
|
||||
# Create the SubmissionsLink instance
|
||||
site_user.content_type=content_type
|
||||
site_user.object_id=site_user_id
|
||||
|
||||
site_user.save()
|
||||
|
||||
author.save()
|
||||
|
||||
return author, site_user
|
||||
|
||||
|
||||
def import_twitter_tags(self, data: dict, category: str) -> list[Twitter_Tags]:
|
||||
"""
|
||||
Import a Twitter tag from the provided data into the database.
|
||||
|
||||
Parameters:
|
||||
data (dict): The data containing information about the Twitter tag.
|
||||
|
||||
Returns:
|
||||
list[Twitter_Tags]: A list of imported or retrieved Twitter tag objects.
|
||||
"""
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Twitter_Tags)
|
||||
|
||||
tags: list[Twitter_Tags] = []
|
||||
|
||||
if "hashtags" in data:
|
||||
for t_tag_name in data["hashtags"]:
|
||||
t_tag_slug = slugify(t_tag_name)
|
||||
try:
|
||||
# Check if the tag already exists in the database by name
|
||||
tag: Twitter_Tags = Twitter_Tags.objects.get(tag_slug=t_tag_slug)
|
||||
|
||||
tag_id = tag.pk
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
# If the tag does not exist, create a new tag and generate the slug
|
||||
tag = Twitter_Tags(tag=t_tag_name)
|
||||
tag.tag_slug = t_tag_slug
|
||||
|
||||
tag_id = tag.pk
|
||||
|
||||
site_tags, created = Tags.objects.get_or_create(tag_slug=t_tag_slug)
|
||||
|
||||
site_tags.category.add(category)
|
||||
|
||||
site_tags.content_type=content_type
|
||||
site_tags.object_id=tag_id
|
||||
|
||||
site_tags.save()
|
||||
|
||||
tag.save() # Save the tag (either new or existing)
|
||||
|
||||
tags.append(tag)
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def import_from_twitter(self, data, json_file_path, delete):
|
||||
|
||||
category, created = Category.objects.get_or_create(name=data['category'])
|
||||
|
||||
category.save()
|
||||
|
||||
twitter_submission, created = Twitter_Submissions.objects.get_or_create(submission_id=data["tweet_id"])
|
||||
|
||||
file_path = json_file_path.removesuffix(".json")
|
||||
|
||||
# Handle author import
|
||||
author, site_user = self.import_twitter_user(data, file_path, category, delete)
|
||||
|
||||
twitter_submission.author = author
|
||||
|
||||
# Handle tag import
|
||||
tags = self.import_twitter_tags(data, category)
|
||||
|
||||
for tag in tags:
|
||||
twitter_submission.tags.add(tag) # Add the tag to the submission
|
||||
|
||||
twitter_submission.gallery_type = data['subcategory']
|
||||
|
||||
# Handle file import
|
||||
twitter_submission.files.add(self.import_file(file_path, Submission_File, delete))
|
||||
|
||||
# Handle metadata file import
|
||||
twitter_submission.metadata.add(self.import_file(json_file_path, Metadata_Files, delete))
|
||||
|
||||
twitter_submission.description = data['content']
|
||||
|
||||
twitter_submission.date = timezone.make_aware(datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
twitter_submission.origin_site = data['category']
|
||||
|
||||
twitter_submission.file_extension = data['extension']
|
||||
|
||||
twitter_submission.origin_filename = data['filename']
|
||||
|
||||
if twitter_submission.media_num is None or data['num'] > twitter_submission.media_num:
|
||||
twitter_submission.media_num = data['num']
|
||||
|
||||
if "height" in data.keys():
|
||||
twitter_submission.image_height = data['height']
|
||||
if "width" in data.keys():
|
||||
twitter_submission.image_width = data['width']
|
||||
|
||||
if "sensitive" in data.keys():
|
||||
twitter_submission.sensitive = data['sensitive']
|
||||
|
||||
if "favorite_count" in data.keys():
|
||||
twitter_submission.favorites_count = data['favorite_count']
|
||||
|
||||
if "quote_count" in data.keys():
|
||||
twitter_submission.quote_count = data['quote_count']
|
||||
|
||||
if "reply_count" in data.keys():
|
||||
twitter_submission.reply_count = data['reply_count']
|
||||
|
||||
if "retweet_count" in data.keys():
|
||||
twitter_submission.retweet_count = data['retweet_count']
|
||||
|
||||
twitter_submission.lang = data['lang']
|
||||
|
||||
twitter_submission.save()
|
||||
|
||||
submission_hash = self.compute_string_hash(category.name + data['author']['name'] + str(data["tweet_id"]))
|
||||
|
||||
submission, created = Submissions.objects.get_or_create(submission_hash=submission_hash)
|
||||
|
||||
submission.category = category
|
||||
|
||||
submission.author = site_user
|
||||
|
||||
if twitter_submission.sensitive is not None:
|
||||
submission.mature = twitter_submission.sensitive
|
||||
else:
|
||||
submission.mature = False
|
||||
|
||||
submission.date = timezone.make_aware(datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Twitter_Submissions)
|
||||
|
||||
# Get the primary key of the twitter_submission instance
|
||||
twitter_submission_id = twitter_submission.pk
|
||||
|
||||
# Create the SubmissionsLink instance
|
||||
submission.content_type=content_type
|
||||
submission.object_id=twitter_submission_id
|
||||
|
||||
submission.save()
|
||||
|
||||
self.delete_imported_file(json_file_path, delete)
|
||||
self.delete_imported_file(file_path, delete)
|
||||
|
||||
|
||||
def import_furaffinity_user(self, data, json_file_path, category, delete):
|
||||
content_type = ContentType.objects.get_for_model(FA_User)
|
||||
|
||||
artist, created = FA_User.objects.get_or_create(artist_url=data["artist_url"], artist=data["artist"])
|
||||
|
||||
author_hash = self.compute_string_hash(data["artist_url"] + data['category'])
|
||||
|
||||
site_user, created = Users.objects.get_or_create(user_hash=author_hash)
|
||||
|
||||
site_user.category = category
|
||||
|
||||
|
||||
# Get the primary key of the furaffinity_submission instance
|
||||
site_user_id = artist.pk
|
||||
|
||||
# Create the SubmissionsLink instance
|
||||
site_user.content_type=content_type
|
||||
site_user.object_id=site_user_id
|
||||
|
||||
site_user.save()
|
||||
|
||||
return artist, site_user
|
||||
|
||||
|
||||
def import_furaffinity_tags(self, data, category):
|
||||
|
||||
content_type = ContentType.objects.get_for_model(FA_Tags)
|
||||
|
||||
tags: list[FA_Tags] = []
|
||||
|
||||
site_tags: list[Tags] = []
|
||||
|
||||
if "tags" in data:
|
||||
for t_tag_name in data["tags"]:
|
||||
t_tag_slug = slugify(t_tag_name)
|
||||
try:
|
||||
# Check if the tag already exists in the database by name
|
||||
tag: FA_Tags = FA_Tags.objects.get(tag_slug=t_tag_slug)
|
||||
|
||||
tag_id = tag.pk
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
# If the tag does not exist, create a new tag and generate the slug
|
||||
tag = FA_Tags(tag=t_tag_name)
|
||||
tag.tag_slug = t_tag_slug
|
||||
|
||||
tag_id = tag.pk
|
||||
|
||||
site_tag, created = Tags.objects.get_or_create(tag_slug=t_tag_slug)
|
||||
|
||||
site_tag.category.add(category)
|
||||
|
||||
site_tag.content_type=content_type
|
||||
site_tag.object_id=tag_id
|
||||
|
||||
site_tag.save()
|
||||
|
||||
tag.save() # Save the tag (either new or existing)
|
||||
|
||||
tags.append(tag)
|
||||
|
||||
site_tags.append(site_tag)
|
||||
|
||||
return tags, site_tags
|
||||
|
||||
|
||||
def import_from_furaffinity(self, data, json_file_path, delete):
|
||||
|
||||
category, created = Category.objects.get_or_create(name=data['category'])
|
||||
|
||||
category.save()
|
||||
|
||||
furaffinity_submission, created = FA_Submission.objects.get_or_create(submission_id=data["id"])
|
||||
|
||||
furaffinity_submission.media_url = data["url"]
|
||||
furaffinity_submission.title = data["title"]
|
||||
furaffinity_submission.description = data["description"]
|
||||
|
||||
furaffinity_submission.date = timezone.make_aware(datetime.strptime(data["date"], "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
file_path = json_file_path.removesuffix(".json")
|
||||
|
||||
# Handle author import
|
||||
author, site_user = self.import_furaffinity_user(data, file_path, category, delete)
|
||||
|
||||
furaffinity_submission.artist = author
|
||||
|
||||
# Handle tag import
|
||||
tags, site_tags = self.import_furaffinity_tags(data, category)
|
||||
|
||||
for tag in tags:
|
||||
furaffinity_submission.tags.add(tag) # Add the tag to the submission
|
||||
|
||||
species, created = FA_Species.objects.get_or_create(species=data["species"])
|
||||
furaffinity_submission.species = species
|
||||
|
||||
# Handle mature rating import
|
||||
mature, created = FA_Mature.objects.get_or_create(mature=data["rating"])
|
||||
furaffinity_submission.mature_rating = mature
|
||||
|
||||
furaffinity_submission.number_of_comments = data["comments"]
|
||||
furaffinity_submission.views = data["views"]
|
||||
|
||||
gender, created = FA_Gender.objects.get_or_create(gender=data["gender"])
|
||||
furaffinity_submission.gender = gender
|
||||
|
||||
furaffinity_submission.fa_theme = data["theme"]
|
||||
furaffinity_submission.fa_category = data["fa_category"]
|
||||
furaffinity_submission.gallery_type = data["subcategory"]
|
||||
furaffinity_submission.file_extension = data["extension"]
|
||||
furaffinity_submission.image_height = data["height"]
|
||||
furaffinity_submission.image_width = data["width"]
|
||||
|
||||
# Handle file import
|
||||
furaffinity_submission.files.add(self.import_file(file_path, Submission_File, delete))
|
||||
|
||||
# Handle metadata file import
|
||||
furaffinity_submission.metadata.add(self.import_file(json_file_path, Metadata_Files, delete))
|
||||
|
||||
furaffinity_submission.save()
|
||||
|
||||
submission_hash = self.compute_string_hash(category.name + data["artist_url"] + str(data["id"]))
|
||||
|
||||
submission, created = Submissions.objects.get_or_create(submission_hash=submission_hash)
|
||||
|
||||
submission.category = category
|
||||
|
||||
submission.tags.add(*site_tags)
|
||||
|
||||
submission.author = site_user
|
||||
|
||||
if furaffinity_submission.mature_rating.mature != "General" and not None:
|
||||
print("Mature")
|
||||
submission.mature = True
|
||||
else:
|
||||
submission.mature = False
|
||||
|
||||
submission.date = timezone.make_aware(datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
content_type = ContentType.objects.get_for_model(FA_Submission)
|
||||
|
||||
# Get the primary key of the twitter_submission instance
|
||||
furaffinity_submission_id = furaffinity_submission.pk
|
||||
|
||||
# Create the SubmissionsLink instance
|
||||
submission.content_type=content_type
|
||||
submission.object_id=furaffinity_submission_id
|
||||
|
||||
submission.save()
|
||||
|
||||
self.delete_imported_file(json_file_path, delete)
|
||||
self.delete_imported_file(file_path, delete)
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
|
||||
name = models.CharField(unique=True, max_length=64)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Category")
|
||||
verbose_name_plural = _("Categories")
|
||||
|
||||
def __str__(self):
|
||||
return self.name.capitalize()
|
||||
|
||||
|
||||
class Tags(models.Model):
|
||||
|
||||
tag_slug = models.CharField(unique=True, max_length=64,)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
category = models.ManyToManyField(Category)
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
|
||||
object_id = models.PositiveBigIntegerField(null=True)
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Tag")
|
||||
verbose_name_plural = _("Tags")
|
||||
|
||||
def __str__(self):
|
||||
return self.tag_slug
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
|
||||
user_hash = models.CharField(unique=True, max_length=64,)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True)
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
|
||||
object_id = models.PositiveBigIntegerField(null=True)
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("user")
|
||||
verbose_name_plural = _("Users")
|
||||
|
||||
def __str__(self):
|
||||
return self.content_object.artist
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('sites:artist_profile', args=[self.user_hash])
|
||||
|
||||
|
||||
class Submissions(models.Model):
|
||||
|
||||
submission_hash = models.CharField(unique=True, max_length=64,)
|
||||
date = models.DateTimeField(null=True, editable=True)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True)
|
||||
author = models.ForeignKey(Users, on_delete=models.CASCADE, null=True)
|
||||
|
||||
mature = models.BooleanField(default=False)
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
|
||||
object_id = models.PositiveBigIntegerField(null=True)
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
|
||||
tags = models.ManyToManyField(Tags)
|
||||
|
||||
custom_tags = models.ManyToManyField(CustomTags)
|
||||
|
||||
description_length = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Submission")
|
||||
verbose_name_plural = _("Submissions")
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=['content_type', 'object_id']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.submission_hash
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("sites:submission", args=[self.submission_hash])
|
|
@ -1,55 +0,0 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def is_image(mime_type):
|
||||
"""
|
||||
A function that takes the mime type as input and returns true if it is an image
|
||||
"""
|
||||
return mime_type.startswith("image/")
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_video(mime_type):
|
||||
"""
|
||||
A function that takes the mime type as input and returns true if it is an video
|
||||
"""
|
||||
return mime_type.startswith("video/")
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_flash(mime_type):
|
||||
"""
|
||||
A function that takes the mime type as input and returns true if it is an flash
|
||||
"""
|
||||
|
||||
valid_flash_mime_types = [
|
||||
"application/vnd.adobe.flash.movie",
|
||||
"application/x-shockwave-flash",
|
||||
"application/futuresplash",
|
||||
"application/x-swf",
|
||||
]
|
||||
|
||||
for valid_type in valid_flash_mime_types:
|
||||
if valid_type in mime_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_pdf(mime_type):
|
||||
"""
|
||||
A function that takes the mime type as input and returns true if it is an pdf
|
||||
"""
|
||||
valid_pdf_mime_types = [
|
||||
"application/pdf",
|
||||
"application/vnd.cups-pdf",
|
||||
"application/x-pdf",
|
||||
]
|
||||
|
||||
for valid_type in valid_pdf_mime_types:
|
||||
if valid_type in mime_type:
|
||||
return True
|
||||
|
||||
return False
|
|
@ -1,24 +0,0 @@
|
|||
from django import template
|
||||
|
||||
from utils.strings import (
|
||||
convert_size,
|
||||
aTag_urls,
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def size_to_human_readable (size):
|
||||
"""
|
||||
A filter that converts the given size to a human-readable format using the utils.strings.convert_size function.
|
||||
|
||||
Parameters:
|
||||
size: The size to be converted.
|
||||
|
||||
Returns:
|
||||
The human-readable size format.
|
||||
"""
|
||||
return convert_size(size)
|
||||
|
||||
@register.filter
|
||||
def clickable_urls(string):
|
||||
return aTag_urls(string)
|
|
@ -1,7 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'twitter'
|
||||
label = 'twitter'
|
|
@ -1,67 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} 404 Page Not Found {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
.e-container-border{
|
||||
padding: 4px;
|
||||
margin: auto;
|
||||
background-color:#222222;
|
||||
border-radius: 25px;
|
||||
background: linear-gradient(180deg, #4b8fca, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
box-shadow: 0px 2px 10px 0px #221133;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.3s;
|
||||
display: flex;
|
||||
}
|
||||
.e-container{
|
||||
overflow:hidden;
|
||||
background-color:#222222;
|
||||
padding:24px;
|
||||
text-align:justify;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
{% comment %}
|
||||
.containcenter{
|
||||
margin:auto;
|
||||
}
|
||||
.index{
|
||||
transform: scale(0.95);
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.3s;
|
||||
filter: saturate(0) contrast(75%) brightness(0.8);
|
||||
}
|
||||
.index:hover{
|
||||
transform: scale(1);
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.1s;
|
||||
filter: saturate(1) contrast(100%) brightness(1);
|
||||
}
|
||||
|
||||
{% endcomment %}
|
||||
</style>
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container">
|
||||
<div class="e-container-border row mb-3" tabindex="1">
|
||||
<div class="e-container">
|
||||
<div class="my-20 text-center">
|
||||
<h1 class="bold glitch" data-text="404">404</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
<p>Sorry cant find that page :(</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,82 +0,0 @@
|
|||
{% extends "layouts/base-fullscreen.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Sign In {% endblock title %}
|
||||
|
||||
<!-- Specific Page CSS goes HERE -->
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block body_class %}{% endblock body_class %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<style type="text/css">
|
||||
.body-p{
|
||||
background-image: url('{% static "/img/bg/login-bg.jpg" %}');
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.div-p{
|
||||
max-width:700px;
|
||||
background: rgb(0,0,0);
|
||||
background: -moz-linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
background: -webkit-linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
background: linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#000000",endColorstr="#ffffff",GradientType=1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="d-flex flex-column body-p vh-100" loading="lazy">
|
||||
<div class="d-flex flex-grow-1 div-p">
|
||||
<form class="p-3 bg-body m-auto border border-2 border-info-subtle rounded bg-opacity-75" style="width: 24rem;" role="form" method="post" action="">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<h3 class="fw-normal mb-3 pb-3" style="letter-spacing: 1px;">
|
||||
<i class="nf nf-md-login"></i> Log in
|
||||
</h3>
|
||||
|
||||
<div class="row mb-3">
|
||||
<p class="mb-0 text-danger text-center">
|
||||
{% if msg %}
|
||||
{{ msg | safe }}
|
||||
{% else %}
|
||||
Input your credentials
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="input-group input-group-outline mb-3">
|
||||
{{ form.username }}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-outline mb-3">
|
||||
{{ form.password }}
|
||||
</div>
|
||||
<div class="form-check form-switch d-flex align-items-center mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="rememberMe">
|
||||
<label class="form-check-label mb-0 ms-2" for="rememberMe">Remember me</label>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" name="login"
|
||||
class="btn btn-outline-primary bg-gradient-primary w-100 my-4 mb-2 text-light">Sign in</button>
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-center">
|
||||
Don't have an account?
|
||||
<a href="{% url 'register' %}" class="text-primary text-gradient font-weight-bold">Sign up</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% include "includes/footer-auth.html" %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
<!-- Specific Page JS goes HERE -->
|
||||
{% block javascripts %}{% endblock javascripts %}
|
|
@ -1,66 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Sites {% endblock title %}
|
||||
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="e-container-border e-container-radius row mb-3" tabindex="1">
|
||||
<div class="e-container e-container-radius p-3">
|
||||
|
||||
<h1 class="text-center pb-3">Profile Info</h1>
|
||||
|
||||
<div class="table-responsive rounded-2">
|
||||
<table class="table table-sm table-bordered border-primary-subtle table-striped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="text-center">Last Login</th>
|
||||
<td class="text-center">{{ user.last_login }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-center">Registration Date</th>
|
||||
<td class="text-center">{{ user.date_joined }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-center">Admin Status</th>
|
||||
<td class="text-center">
|
||||
{% if user.is_staff %}
|
||||
<span class="badge bg-success">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="e-container-border e-container-radius row my-3" tabindex="1">
|
||||
<div class="e-container e-container-radius">
|
||||
|
||||
<h1 class="text-center">Profile Settings</h1>
|
||||
|
||||
<h3>Edit your profile</h3>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ user_form.as_p }}
|
||||
{{ profile_form.as_p }}
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,101 +0,0 @@
|
|||
{% extends "layouts/base-fullscreen.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Sign up {% endblock %}
|
||||
|
||||
{% block body_class %}{% endblock %}
|
||||
|
||||
<!-- Specific Page CSS goes HERE -->
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation-transparent.html' %}
|
||||
<style type="text/css">
|
||||
.body-p{
|
||||
background-image: url('{% static "/img/bg/login-bg.jpg" %}');
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.div-p{
|
||||
max-width:700px;
|
||||
background: rgb(0,0,0);
|
||||
background: -moz-linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
background: -webkit-linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
background: linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#000000",endColorstr="#ffffff",GradientType=1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="d-flex flex-column body-p vh-100" loading="lazy">
|
||||
<div class="d-flex flex-grow-1 div-p">
|
||||
<form class="p-3 bg-body m-auto border border-2 border-info-subtle rounded bg-opacity-75" style="width: 24rem;" role="form" method="post" action="">
|
||||
|
||||
<h3 class="fw-normal mb-3 pb-3" style="letter-spacing: 1px;">
|
||||
<i class="nf nf-md-login"></i>
|
||||
Sign up
|
||||
</h3>
|
||||
|
||||
<div class="row mb-3">
|
||||
<p class="mb-0 text-danger text-center">
|
||||
{% if msg %}
|
||||
{{ msg | safe }}
|
||||
{% else %}
|
||||
Enter your email and password to register
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="input-group input-group-outline mb-3">
|
||||
{{ form.username }}
|
||||
</div>
|
||||
<span class="text-danger">{{ form.username.errors }}</span>
|
||||
|
||||
<div class="input-group input-group-outline mb-3">
|
||||
{{ form.email }}
|
||||
</div>
|
||||
<span class="text-danger">{{ form.email.errors }}</span>
|
||||
|
||||
<div class="input-group input-group-outline mb-3">
|
||||
{{ form.password1 }}
|
||||
</div>
|
||||
<span class="text-danger">{{ form.password1.errors }}</span>
|
||||
|
||||
<div class="input-group input-group-outline mb-3">
|
||||
{{ form.password2 }}
|
||||
</div>
|
||||
<span class="text-danger">{{ form.password2.errors }}</span>
|
||||
|
||||
<div class="input-group form-check form-check-info text-start">
|
||||
<input class="form-check-input rounded" type="checkbox" value="" id="flexCheckDefault">
|
||||
<label class="form-check-label ps-2" for="flexCheckDefault">
|
||||
I agree the <a href="javascript:;" class="text-info font-weight-bolder">Terms and Conditions</a>
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" name="register"
|
||||
class="btn btn-outline-primary bg-gradient-primary w-100 mt-4 mb-0 text-body-emphasis">Sign up</button>
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-center">
|
||||
Already have an account?
|
||||
<a href="{% url 'login' %}" class="text-primary text-gradient font-weight-bold">Sign in</a>
|
||||
</p>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% include 'includes/footer-auth.html' %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
<!-- Specific Page JS goes HERE -->
|
||||
{% block javascripts %}{% endblock javascripts %}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
{% extends "layouts/base-fullscreen.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Upload {% endblock %}
|
||||
|
||||
<!-- Specific Page CSS goes HERE -->
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block body_class %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation-transparent.html' %}
|
||||
|
||||
<style type="text/css">
|
||||
.body-p{
|
||||
background-image: url('{% static "/img/bg/login-bg.jpg" %}');
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.div-p{
|
||||
max-width:700px;
|
||||
background: rgb(0,0,0);
|
||||
background: -moz-linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
background: -webkit-linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
background: linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0.6404762588629201) 35%, rgba(255,255,255,0) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#000000",endColorstr="#ffffff",GradientType=1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="d-flex flex-column body-p vh-100" loading="lazy">
|
||||
<div class="d-flex flex-grow-1 div-p">
|
||||
<form class="p-3 bg-body m-auto border border-2 border-info-subtle rounded bg-opacity-75" style="width: 24rem;" role="form" method="post" action="" enctype="multipart/form-data">
|
||||
{% if error_message %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary bg-gradient-primary w-100 my-4 mb-2 text-light">
|
||||
Upload
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% comment %} {% include 'includes/footer.html' %} {% endcomment %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
<!-- Specific Page JS goes HERE -->
|
||||
{% block javascripts %}{% endblock javascripts %}
|
|
@ -1,55 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Importer {% endblock title %}
|
||||
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row row-gap-3">
|
||||
|
||||
<div class="col">
|
||||
<div class="e-container-border e-container-radius">
|
||||
<div class="e-container e-container-radius p-2 pt-3 mb-3">
|
||||
<h1 class="text-center">Importer</h1>
|
||||
|
||||
{% include "importer/partials/tabnavbar.html" %}
|
||||
|
||||
<form class="p-3 m-auto border border-2 border-info-subtle rounded gap-2" style="width: 24rem;" role="form" method="post" action="">
|
||||
|
||||
{% if ImportURLFormMSG %}
|
||||
<div class="row mb-3">
|
||||
<p class="mb-0 text-danger text-center">
|
||||
{{ ImportURLFormMSG | safe }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="input-group mb-3" {% if form.url.errors %} style="border-color: red" {% endif %}>
|
||||
<label class="input-group-text" for="{{ form.url.id_for_label }}">{{ ImportURLForm.url.label }}</label>
|
||||
{{ ImportURLForm.url }}
|
||||
</div>
|
||||
{% comment %} <span class="input-group-text" id="basic-addon1">URL</span> {% endcomment %}
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,21 +0,0 @@
|
|||
<ul class="nav nav-tabs mb-3">
|
||||
{% for tab in tabs %}
|
||||
<li class="nav-item">
|
||||
{% if not tab.adminOnly %}
|
||||
{% url tab.url as tab_url %}
|
||||
{% if tab_url == request.path %}
|
||||
<a class="nav-link active" aria-current="page" href="{% url tab.url %}">{{ tab.name }}</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{% url tab.url %}">{{ tab.name }}</a>
|
||||
{% endif %}
|
||||
{% elif request.user.is_staff and request.user.is_superuser %}
|
||||
{% url tab.url as tab_url %}
|
||||
{% if tab_url == request.path %}
|
||||
<a class="nav-link active" aria-current="page" href="{% url tab.url %}">{{ tab.name }}</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{% url tab.url %}">{{ tab.name }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -1,252 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Source URLs | Importer {% endblock title %}
|
||||
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row row-gap-3">
|
||||
|
||||
<div class="col">
|
||||
<div class="e-container-border e-container-radius">
|
||||
<div class="e-container e-container-radius p-2 pt-3 mb-3">
|
||||
<h1 class="text-center">Source URLs</h1>
|
||||
|
||||
{% include "importer/partials/tabnavbar.html" %}
|
||||
|
||||
<h3>Complete Profiles/Galleries</h3>
|
||||
|
||||
<input type="text" class="form-control input-group-text my-2" onkeyup="filterTable('profiles', 0, this.value)" placeholder="Search for URLs...">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="profiles" class="table table-sm table-striped table-bordered table-responsive table-striped tabel-hover text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL:<span class="ms-1 text-primary">↕</span></th>
|
||||
<th>Category:<span class="ms-1 text-primary">↕</span></th>
|
||||
<th>Added On:<span class="ms-1 text-primary">↕</span></th>
|
||||
<th data-bs-toggle="tooltip" title="Last Imported/Scaned">Imported:<span class="ms-1 text-primary">↕</span></th>
|
||||
{% if user.is_superuser or user.is_staff %}
|
||||
<th>Added By:<span class="ms-1 text-primary">↕</span></th>
|
||||
{% endif %}
|
||||
<th>Active:<span class="ms-1 text-primary">↕</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for url in SourceURLs %}
|
||||
{% if url.source_type == "C" %}
|
||||
<tr id="C-{{ url.pk }}">
|
||||
<td>{{ url.url }}</td>
|
||||
<td>{{ url.category }}</td>
|
||||
<td data-timestamp="{{ url.date_added|date:'U' }}" data-bs-toggle="tooltip" title="{{ url.date_added|date:'Y-m-d H:i' }}">{{ url.date_added|date:'Y-m-d' }}</td>
|
||||
<td data-timestamp="{{ url.last_imported|date:'U' }}" data-bs-toggle="tooltip" title="{{ url.last_imported|date:'Y-m-d H:i' }}">{{ url.last_imported|date:'Y-m-d' }}</td>
|
||||
{% if user.is_superuser or user.is_staff %}
|
||||
<td>{{ url.added_by_user|capfirst }}</td>
|
||||
{% endif %}
|
||||
<td>{{ url.active }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<h3>Single Posts</h3>
|
||||
|
||||
<input type="text" class="form-control input-group-text my-2" onkeyup="filterTable('posts', 0, this.value)" placeholder="Search for URLs...">
|
||||
|
||||
<table id="posts" class="table table-bordered table-responsive table-striped tabel-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL:</th>
|
||||
<th>Category:</th>
|
||||
<th>Added On:</th>
|
||||
<th>Last Imported/Scaned:</th>
|
||||
{% if user.is_superuser or user.is_staff %}
|
||||
<th>Added By</th>
|
||||
{% endif %}
|
||||
<th>Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for url in SourceURLs %}
|
||||
{% if url.source_type == "P" %}
|
||||
<tr>
|
||||
<td>{{ url.url }}</td>
|
||||
<td>{{ url.category }}</td>
|
||||
<td>{{ url.date_added|date:'Y-m-d H:i' }}</td>
|
||||
<td>{{ url.last_imported|date:'Y-m-d H:i' }}</td>
|
||||
{% if user.is_superuser or user.is_staff %}
|
||||
<td>{{ url.added_by_user|capfirst }}</td>
|
||||
{% endif %}
|
||||
<td>{{ url.active }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterTable(tableId, columnIndex, input) {
|
||||
console.log(tableId, columnIndex, input.value);
|
||||
// Declare variables
|
||||
var filter = input.toUpperCase();
|
||||
var table = document.getElementById(tableId);
|
||||
var rows = table.getElementsByTagName("tr");
|
||||
|
||||
// Loop through all table rows, and hide those who don't match the search query
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var cells = rows[i].getElementsByTagName("td");
|
||||
if (cells.length) {
|
||||
var txtValue = cells[columnIndex].textContent || cells[columnIndex].innerText;
|
||||
if (txtValue.toUpperCase().indexOf(filter) > -1) {
|
||||
rows[i].style.display = "";
|
||||
} else {
|
||||
rows[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortTable(tableId, n) {
|
||||
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
|
||||
table = document.getElementById(tableId);
|
||||
switching = true;
|
||||
// Set the sorting direction to ascending:
|
||||
dir = "asc";
|
||||
/* Make a loop that will continue until
|
||||
no switching has been done: */
|
||||
while (switching) {
|
||||
// Start by saying: no switching is done:
|
||||
switching = false;
|
||||
rows = table.rows;
|
||||
/* Loop through all table rows (except the
|
||||
first, which contains table headers): */
|
||||
for (i = 1; i < (rows.length - 1); i++) {
|
||||
// Start by saying there should be no switching:
|
||||
shouldSwitch = false;
|
||||
/* Get the two elements you want to compare,
|
||||
one from current row and one from the next: */
|
||||
x = rows[i].getElementsByTagName("TD")[n];
|
||||
y = rows[i + 1].getElementsByTagName("TD")[n];
|
||||
/* Check if the two rows should switch place,
|
||||
based on the direction, asc or desc: */
|
||||
if (dir == "asc") {
|
||||
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
|
||||
// If so, mark as a switch and break the loop:
|
||||
shouldSwitch = true;
|
||||
break;
|
||||
}
|
||||
} else if (dir == "desc") {
|
||||
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
|
||||
// If so, mark as a switch and break the loop:
|
||||
shouldSwitch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldSwitch) {
|
||||
/* If a switch has been marked, make the switch
|
||||
and mark that a switch has been done: */
|
||||
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
|
||||
switching = true;
|
||||
// Each time a switch is done, increase this count by 1:
|
||||
switchcount ++;
|
||||
} else {
|
||||
/* If no switching has been done AND the direction is "asc",
|
||||
set the direction to "desc" and run the while loop again. */
|
||||
if (switchcount == 0 && dir == "asc") {
|
||||
dir = "desc";
|
||||
switching = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
document.querySelectorAll('th').forEach((element) => { // Table headers
|
||||
element.addEventListener('click', function() {
|
||||
let table = this.closest('table');
|
||||
|
||||
// If the column is sortable
|
||||
if (this.querySelector('span')) {
|
||||
let order_icon = this.querySelector('span');
|
||||
let order = encodeURI(order_icon.innerHTML).includes('%E2%86%91') ? 'desc' : 'asc';
|
||||
let separator = '-----'; // Separate the value of it's index, so data keeps intact
|
||||
|
||||
let value_list = {}; // <tr> Object
|
||||
let obj_key = []; // Values of selected column
|
||||
|
||||
let string_count = 0;
|
||||
let number_count = 0;
|
||||
|
||||
// <tbody> rows
|
||||
table.querySelectorAll('tbody tr').forEach((line, index_line) => {
|
||||
// Value of each field
|
||||
let key = line.children[element.cellIndex].textContent.toUpperCase();
|
||||
|
||||
// Check if value is date, numeric or string
|
||||
if (line.children[element.cellIndex].hasAttribute('data-timestamp')) {
|
||||
// if value is date, we store it's timestamp, so we can sort like a number
|
||||
key = line.children[element.cellIndex].getAttribute('data-timestamp');
|
||||
}
|
||||
else if (key.replace('-', '').match(/^[0-9,.]*$/g)) {
|
||||
number_count++;
|
||||
}
|
||||
else {
|
||||
string_count++;
|
||||
}
|
||||
|
||||
value_list[key + separator + index_line] = line.outerHTML.replace(/(\t)|(\n)/g, ''); // Adding <tr> to object
|
||||
obj_key.push(key + separator + index_line);
|
||||
});
|
||||
if (string_count === 0) { // If all values are numeric
|
||||
console.log(obj_key);
|
||||
obj_key.sort(function(a, b) {
|
||||
return a.split(separator)[0] - b.split(separator)[0];
|
||||
});
|
||||
console.log(obj_key);
|
||||
}
|
||||
else {
|
||||
console.log(obj_key);
|
||||
obj_key.sort();
|
||||
console.log(obj_key);
|
||||
}
|
||||
|
||||
if (order === 'desc') {
|
||||
console.log(obj_key);
|
||||
obj_key.reverse();
|
||||
console.log(obj_key);
|
||||
order_icon.innerHTML = '↓';
|
||||
}
|
||||
else {
|
||||
order_icon.innerHTML = '↑';
|
||||
}
|
||||
|
||||
let html = '';
|
||||
obj_key.forEach(function(chave) {
|
||||
html += value_list[chave];
|
||||
});
|
||||
table.getElementsByTagName('tbody')[0].innerHTML = html;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,22 +0,0 @@
|
|||
<!-- Start footer auth -->
|
||||
<footer class="footer bg-body-tertiary bottom-2 py-2 w-100 z-index-3 border border-top-1">
|
||||
<div class="container text-body-secondary">
|
||||
<div class="row align-items-center justify-content-lg-between">
|
||||
<div class="col-12 col-md-6 my-auto">
|
||||
<div class="copyright text-center text-sm text-lg-start">
|
||||
©
|
||||
<a href="https://aroy-art.com" class="font-weight-bold" target="_blank">Aroy Art</a>
|
||||
and Contributors.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="nav nav-footer justify-content-center justify-content-lg-end">
|
||||
<li class="nav-item">
|
||||
<a href="https://git.aroy-art.com/Aroy/Gallery-Archivist" class="nav-link" target="_blank">What is this? - Source</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- End footer auth -->
|
|
@ -1,99 +0,0 @@
|
|||
|
||||
{% load static %}
|
||||
|
||||
{% load media_filters %}
|
||||
|
||||
{% load string_helper %}
|
||||
|
||||
{% load thumbnail %}
|
||||
|
||||
<div class="gallery-container">
|
||||
{% for submission in submissions %}
|
||||
<div class="gallery-item bg-dark">
|
||||
|
||||
{% include "sites/partials/site-btn-overlay.html" with category=submission.category.name %}
|
||||
|
||||
<span class="seen-overlay text-primary-emphasis" data-seen="false" data-hash="{{ submission.submission_hash }}" href=''></span>
|
||||
|
||||
{% if submission.content_object.files.exists %}
|
||||
|
||||
{% if submission.content_object.files.first.file_mime|is_image %}
|
||||
|
||||
{% if submission.content_object.files.all|length == 1 %}
|
||||
{% thumbnail submission.content_object.files.first.file "350" as im %}
|
||||
{% if submission.mature == True and user_profile.show_mature == "B" %}
|
||||
<img src="{{ im.url }}" alt="{{ submission.content_object.files.first.file_name }}" height="100%" class="blur">
|
||||
{% else %}
|
||||
<img src="{{ im.url }}" alt="{{ submission.content_object.files.first.file_name }}" height="100%">
|
||||
{% endif %}
|
||||
{% endthumbnail %}
|
||||
|
||||
{% elif submission.content_object.files.all|length == 2 %}
|
||||
{% for file in submission.content_object.files.all %}
|
||||
{% thumbnail file.file "350" as im %}
|
||||
{% if submission.mature == True and user_profile.show_mature == "B" %}
|
||||
<img src="{{ im.url }}" alt="{{ file.file_name }}" height="100%" class="blur">
|
||||
{% else %}
|
||||
<img src="{{ im.url }}" alt="{{ file.file_name }}" height="100%">
|
||||
{% endif %}
|
||||
{% endthumbnail %}
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
{% for file in submission.content_object.files.all %}
|
||||
{% thumbnail file.file "350" as im %}
|
||||
<div class="col">
|
||||
{% if submission.mature == True and user_profile.show_mature == "B" %}
|
||||
<img src="{{ im.url }}" alt="{{ file.file_name }}" height="100%" class="blur">
|
||||
{% else %}
|
||||
<img src="{{ im.url }}" alt="{{ file.file_name }}" height="100%">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endthumbnail %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% elif submission.content_object.files.first.file_mime|is_video %}
|
||||
|
||||
<video class="gallery-item" src="{% url 'files:serve_content_file' 'submission' submission.content_object.files.first.file_hash %}" controlsList="nodownload"></video>
|
||||
|
||||
{% else %}
|
||||
<img src="{% static 'img/placeholder/no-image-dark.webp' %}">
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
|
||||
<span class="badge bg-secondary">This submission has no media</span>
|
||||
|
||||
{% endif %}
|
||||
<a href='{% url "sites:submission" submission.submission_hash %}' class="stretched-link"></a>
|
||||
<div class="overlay p-2 text-center">
|
||||
{% if submission.content_object.title %}
|
||||
{% if submission.content_object.title|length > 64 %}
|
||||
<p title="{{ submission.content_object.title }}">{{ submission.content_object.title|slice:"0:64"|add:"..." }}</p>
|
||||
{% else %}
|
||||
<p title="{{ submission.content_object.title }}">{{ submission.content_object.title }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% if submission.content_object.description|length > 64 %}
|
||||
<p>{{ submission.content_object.description|html_to_text|slice:"0:64"|add:"..." }}</p>
|
||||
{% else %}
|
||||
<p>{{ submission.content_object.description|html_to_text }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
<a href="{% url 'sites:artist_profile' submission.author.user_hash %}" class="z-2">
|
||||
{% if submission.category.name == "furaffinity" %}
|
||||
{{ submission.content_object.artist.artist }}
|
||||
{% else %}
|
||||
{{ submission.content_object.author.artist }}
|
||||
{% endif %}
|
||||
</a>
|
||||
<small class="badge bg-secondary">{{ submission.content_object.date|date:'Y-m-d H:i:s' }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -1,196 +0,0 @@
|
|||
<!-- Navbar Transparent -->
|
||||
<nav class="navbar navbar-expand-lg fixed-top top-0 z-index-3 w-100 shadow-none my-3 text-body">
|
||||
<div class="container bg-body-secondary p-2 rounded bg-opacity-75 shadow">
|
||||
|
||||
<!-- svg icons -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
|
||||
<symbol id="bootstrap" viewBox="0 0 512 408" fill="currentcolor">
|
||||
<path d="M106.342 0c-29.214 0-50.827 25.58-49.86 53.32.927 26.647-.278 61.165-8.966 89.31C38.802 170.862 24.07 188.707 0 191v26c24.069 2.293 38.802 20.138 47.516 48.37 8.688 28.145 9.893 62.663 8.965 89.311C55.515 382.42 77.128 408 106.342 408h299.353c29.214 0 50.827-25.58 49.861-53.319-.928-26.648.277-61.166 8.964-89.311 8.715-28.232 23.411-46.077 47.48-48.37v-26c-24.069-2.293-38.765-20.138-47.48-48.37-8.687-28.145-9.892-62.663-8.964-89.31C456.522 25.58 434.909 0 405.695 0H106.342zm236.559 251.102c0 38.197-28.501 61.355-75.798 61.355h-87.202a2 2 0 01-2-2v-213a2 2 0 012-2h86.74c39.439 0 65.322 21.354 65.322 54.138 0 23.008-17.409 43.61-39.594 47.219v1.203c30.196 3.309 50.532 24.212 50.532 53.085zm-84.58-128.125h-45.91v64.814h38.669c29.888 0 46.373-12.03 46.373-33.535 0-20.151-14.174-31.279-39.132-31.279zm-45.91 90.53v71.431h47.605c31.12 0 47.605-12.482 47.605-35.941 0-23.46-16.947-35.49-49.608-35.49h-45.602z"/>
|
||||
</symbol>
|
||||
<symbol id="check2" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
</symbol>
|
||||
<symbol id="circle-half" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/>
|
||||
</symbol>
|
||||
<symbol id="moon-stars-fill" viewBox="0 0 16 16">
|
||||
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
|
||||
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
|
||||
</symbol>
|
||||
<symbol id="sun-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- svg icons end -->
|
||||
|
||||
<a class="navbar-brand ms-2" href="{% url 'home' %}" rel="tooltip" title="Designed and Coded by Aroy" data-placement="bottom">
|
||||
Gallery Archivists
|
||||
</a>
|
||||
<button class="navbar-toggler shadow-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#navigation" aria-controls="navigation" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon my-1">
|
||||
<span class="navbar-toggler-bar bar1"></span>
|
||||
<span class="navbar-toggler-bar bar2"></span>
|
||||
<span class="navbar-toggler-bar bar3"></span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse w-100 pt-3 pb-2 py-lg-0 ms-lg-12 ps-lg-5" id="navigation">
|
||||
<ul class="navbar-nav navbar-nav-hover ms-auto">
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
|
||||
<!-- Search form -->
|
||||
<li class="nav-item ms-lg-auto mx-2">
|
||||
<form class="d-flex" role="search" method="get" action="{% url 'sites:browse' %}">
|
||||
<div class="input-group me-2">
|
||||
<input class="form-control" type="search" name="q" placeholder="Search" aria-label="Search">
|
||||
<button class="input-group-text nf nf-fa-search" id="search-addon" type="submit"></button>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
||||
<hr class="d-lg-none my-2">
|
||||
</li>
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<li class="nav-item dropdown ms-auto mx-auto">
|
||||
<div class="d-flex align-items-center dropdown-center ">
|
||||
<button class="btn btn-link text-body-emphasis px-0 text-decoration-none dropdown-toggle d-flex align-items-center icon-link"
|
||||
id="bd-theme"
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-display="static"
|
||||
aria-label="Toggle theme">
|
||||
<svg class="bi my-1 theme-icon-active"><use href="#circle-half"></use></svg>
|
||||
<span class="d-lg-none ms-0" id="bd-theme-text"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="bd-theme" >
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center icon-link" data-bs-theme-value="light">
|
||||
<svg class="bi me-2 opacity-50 theme-icon"><use href="#sun-fill"></use></svg>
|
||||
Light
|
||||
<svg class="bi ms-auto d-none"><use href="#check2"></use></svg>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center icon-link" data-bs-theme-value="dark">
|
||||
<svg class="bi me-2 opacity-50 theme-icon"><use href="#moon-stars-fill"></use></svg>
|
||||
Dark
|
||||
<svg class="bi ms-auto d-none"><use href="#check2"></use></svg>
|
||||
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center active icon-link" data-bs-theme-value="auto">
|
||||
<svg class="bi me-2 opacity-50 theme-icon"><use href="#circle-half"></use></svg>
|
||||
Auto
|
||||
<svg class="bi ms-auto d-none"><use href="#check2"></use></svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<!-- End Theme Selector -->
|
||||
|
||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
||||
<hr class="d-lg-none my-2">
|
||||
</li>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<li class="nav-item ms-lg-auto mx-2 dropdown">
|
||||
<a class="nav-link dropdown-toggle text-body-emphasis" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% if user.first_name %}
|
||||
{{ user.first_name }}
|
||||
{% else %}
|
||||
{{ user.username }}
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'profile' %}">
|
||||
<i class="nf nf-fa-user"></i>
|
||||
<p class="d-inline font-weight-bold" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Settings">User Settings</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
{% if user.is_superuser %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'admin:index' %}">
|
||||
<i class="nf nf-fa-warning"></i>
|
||||
<p class="d-inline font-weight-bold" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Django Admin">Djnago Admin</p>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'logout' %}">
|
||||
<i class="nf nf-md-logout"></i>
|
||||
<p class="d-inline font-weight-bold" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Sign out">Logout</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- End User Dropdown -->
|
||||
|
||||
{% else %}
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<li class="nav-item ms-lg-auto mx-2">
|
||||
<div class="d-flex align-items-center dropdown color-modes">
|
||||
<button class="btn btn-link text-body px-0 me-2 text-decoration-none dropdown-toggle d-flex align-items-center icon-link"
|
||||
id="bd-theme"
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-display="static"
|
||||
aria-label="Toggle theme">
|
||||
<svg class="bi my-1 theme-icon-active"><use href="#circle-half"></use></svg>
|
||||
<span class="ms-2" id="bd-theme-text">Theme</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center icon-link" data-bs-theme-value="light">
|
||||
<svg class="bi me-2 opacity-50 theme-icon"><use href="#sun-fill"></use></svg>
|
||||
Light
|
||||
<svg class="bi ms-auto d-none"><use href="#check2"></use></svg>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center icon-link" data-bs-theme-value="dark">
|
||||
<svg class="bi me-2 opacity-50 theme-icon"><use href="#moon-stars-fill"></use></svg>
|
||||
Dark
|
||||
<svg class="bi ms-auto d-none"><use href="#check2"></use></svg>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center active icon-link" data-bs-theme-value="auto">
|
||||
<svg class="bi me-2 opacity-50 theme-icon"><use href="#circle-half"></use></svg>
|
||||
Auto
|
||||
<svg class="bi ms-auto d-none"><use href="#check2"></use></svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<!-- End Theme Selector -->
|
||||
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- End Navbar -->
|
|
@ -1,90 +0,0 @@
|
|||
|
||||
<!-- Pagination -->
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{{ request.GET.urlencode }}&page=1" aria-label="First">
|
||||
«
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{{ request.GET.urlencode }}&page={{ page_obj.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">
|
||||
‹
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link text-decoration-line-through">
|
||||
«
|
||||
</span>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link text-decoration-line-through">
|
||||
‹
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link text-nowrap">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{{ request.GET.urlencode }}&page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">
|
||||
›
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{{ request.GET.urlencode }}&page={{ page_obj.paginator.num_pages }}" aria-label="Last">
|
||||
»
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link text-decoration-line-through">
|
||||
›
|
||||
</span>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link text-decoration-line-through">
|
||||
»
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Jump-to field -->
|
||||
<div class="col ">
|
||||
<form method="get" class="d-flex flex-row flex-nowrap flex-grow-1 justify-content-center ">
|
||||
{% for key, value in request.GET.items %}
|
||||
{% if key != "page" %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<label for="jumpToPage" class="col-auto col-form-label mx-1">Jump to:</label>
|
||||
<input type="number" name="page" id="jumpToPage" class="form-control mx-1" min="1" max="{{ page_obj.paginator.num_pages }}" value="{{ page_obj.number }}" style="width: 5rem">
|
||||
<button type="submit" class="btn btn-primary mx-1">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Jump to field End -->
|
||||
|
||||
</div>
|
||||
<!-- Pagination End -->
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
{% load static %}
|
||||
|
||||
<!-- Core JS Files -->
|
||||
|
||||
<script src="{% static 'libs/bootstrap/bootstrap.bundle.min.js' %}" type="text/javascript"></script>
|
||||
|
||||
<!-- Htmx JS & Extentions -->
|
||||
<script src="{% static 'libs/htmx/htmx.min.js' %}" type="text/javascript"></script>
|
||||
|
||||
{% if DEBUG %}
|
||||
<!-- Htmx Debug JS -->
|
||||
<script src="{% static 'libs/htmx/debug.js' %}" type="text/javascript"></script>
|
||||
{% endif %}
|
|
@ -1,74 +0,0 @@
|
|||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" itemscope itemtype="http://schema.org/WebPage" data-bs-theme="auto">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
|
||||
<meta name="description" content="{% block meta_description %}{% endblock meta_description %}">
|
||||
<meta name="keywords" content="{% block meta_keywords %}{% endblock meta_keywords %}">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ ASSETS_ROOT }}/img/apple-icon.png"/>
|
||||
<link rel="icon" type="image/png" href='{% static "/img/favicon.png" %}'/>
|
||||
|
||||
<title>
|
||||
{% block title %}{% endblock title %} - Gallery-Archivists
|
||||
</title>
|
||||
|
||||
<!-- Fonts and icons -->
|
||||
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900|Roboto+Slab:400,700" />
|
||||
|
||||
<!-- Nerd Fonts-->
|
||||
<link rel="stylesheet" href="{% static 'libs/nerdfonts/nerd-fonts-generated.min.css' %}"/>
|
||||
|
||||
<!-- CSS Files -->
|
||||
<link rel="stylesheet" href="{% static 'libs/bootstrap/bootstrap.min.css' %}"/>
|
||||
|
||||
<!-- Main CSS File -->
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}"/>
|
||||
|
||||
<!-- Specific Page CSS goes HERE -->
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
</head>
|
||||
<body class="{% block body_class %}{% endblock body_class %}" hx-header='{% block hx_header %}{% endblock hx_header %} {"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<div class="everything">
|
||||
<div class="wires">
|
||||
<!-- Content -->
|
||||
{% block content %}{% endblock content %}
|
||||
<!-- End Content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="externalLinkConfirmationModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" aria-labelledby="externalLinkConfirmationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h5 class="modal-title" id="externalLinkConfirmationModalLabel">Confirmation</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-body-emphasis">
|
||||
<p>You are leaving this site and visiting an external link. Do you want to proceed?</p>
|
||||
<p id="externalLinkShow"></p>
|
||||
</div>
|
||||
<div class="modal-footer bg-warning">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a id="externalLink" href="#" target="_blank" rel="noopener noreferrer" class="btn btn-primary">Proceed</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "includes/scripts.html" %}
|
||||
|
||||
<script src='{% static "js/confirm_external_links.js" %}'></script>
|
||||
<script src='{% static "js/color-modes.js" %}'></script>
|
||||
<script src='{% static "js/main.js" %}'></script>
|
||||
|
||||
<!-- Specific Page JS goes HERE -->
|
||||
{% block scripts %}{% endblock scripts %}
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,65 +0,0 @@
|
|||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" itemscope itemtype="http://schema.org/WebPage" data-bs-theme="auto">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ ASSETS_ROOT }}/img/apple-icon.png">
|
||||
<link rel="icon" type="image/png" href="{{ ASSETS_ROOT }}/img/favicon.png">
|
||||
|
||||
<title>
|
||||
{% block title %}{% endblock title %} - Gallery-Archivists
|
||||
</title>
|
||||
|
||||
<!-- Fonts and icons -->
|
||||
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900|Roboto+Slab:400,700" />
|
||||
|
||||
<!-- Nerd Fonts-->
|
||||
<link rel="stylesheet" href="{% static 'libs/nerdfonts/nerd-fonts-generated.min.css' %}">
|
||||
|
||||
<!-- CSS Files -->
|
||||
<link rel="stylesheet" href="{% static 'libs/bootstrap/bootstrap.min.css' %}">
|
||||
|
||||
<!-- Specific Page CSS goes HERE -->
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
</head>
|
||||
|
||||
<body class="{% block body_class %} {% endblock body_class %} ">
|
||||
<!-- Content -->
|
||||
{% block content %}{% endblock content %}
|
||||
<!-- End Content -->
|
||||
|
||||
|
||||
<div class="modal fade" id="externalLinkConfirmationModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" aria-labelledby="externalLinkConfirmationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h5 class="modal-title" id="externalLinkConfirmationModalLabel">Confirmation</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You are leaving this site and visiting an external link. Do you want to proceed?</p>
|
||||
<p id="externalLinkShow"></p>
|
||||
</div>
|
||||
<div class="modal-footer bg-warning">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a id="externalLink" href="#" target="_blank" rel="noopener noreferrer" class="btn btn-primary">Proceed</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='{% static "js/confirm_external_links.js" %}'></script>
|
||||
<script src='{% static "js/color-modes.js" %}'></script>
|
||||
<script src='{% static "js/main.js" %}'></script>
|
||||
|
||||
{% include "includes/scripts.html" %}
|
||||
|
||||
<!-- Specific Page JS goes HERE -->
|
||||
{% block javascripts %}{% endblock javascripts %}
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,90 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Browse{% endblock title %}
|
||||
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row row-gap-3 column-gap-0">
|
||||
|
||||
<div class="col-xl-9 col-lg-8 pe-lg-0">
|
||||
<div class="e-container-border e-container-radius">
|
||||
<div class="e-container e-container-radius p-2 pt-3 mb-3">
|
||||
<h1 class="text-center">Browse</h1>
|
||||
|
||||
<hr>
|
||||
|
||||
{% include "includes/pageination.html" with page_obj=submissions %}
|
||||
|
||||
{% include "includes/gallery.html" with user_profile=user_profile %}
|
||||
|
||||
{% include "includes/pageination.html" with page_obj=submissions %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-lg-4">
|
||||
<div class="e-container-border e-container-radius d-none d-sm-none d-md-none d-lg-block ">
|
||||
<div class="e-container e-container-radius p-2 pt-3 mb-3 ">
|
||||
<h1 class="text-center">Search</h1>
|
||||
|
||||
<hr>
|
||||
|
||||
<form class="d-flex flex-column gap-2 bg-body-secondary p-2 rounded" role="search" method="get" action="{% url 'sites:browse' %}">
|
||||
|
||||
<div class="input-group">
|
||||
{{ form.q }}
|
||||
<button class="input-group-text nf nf-fa-search" id="search-addon" type="submit"></button>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="type">{{ form.sort.label }}:</label>
|
||||
<div class="form-control pt-2">
|
||||
{% for radio in form.sort %}
|
||||
<div class="form-check form-check-inline">
|
||||
<label for="{{ radio.id_for_label }}">{{ radio.choice_label }}</label>
|
||||
<input class="form-check-input" type="radio" name="sort" value="{{ radio.data.value }}" id="{{ radio.id_for_label }}" {% if radio.data.selected %}checked{% endif %}>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="category">{{ form.category.label }}:</label>
|
||||
{{ form.category }}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="mature">{{ form.mature.label }}:</label>
|
||||
|
||||
<div class="form-control pt-2">
|
||||
|
||||
{% for radio in form.mature %}
|
||||
<div class="form-check form-check-inline">
|
||||
<label for="{{ radio.id_for_label }}">{{ radio.choice_label }}</label>
|
||||
<input class="form-check-input" type="radio" name="mature" value="{{ radio.data.value }}" id="{{ radio.id_for_label }}" {% if radio.data.selected %}checked{% endif %}>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,117 +0,0 @@
|
|||
{% load string_helper %}
|
||||
|
||||
<h1 class="text-center m-0">Post Info:</h1>
|
||||
|
||||
<hr class="m-1">
|
||||
|
||||
<div class="m-2 overflow-hidden">
|
||||
<strong class="text-info">Tags: </strong>
|
||||
{% if submission.content_object.tags.exists %}
|
||||
{% for tag in submission.content_object.tags.all %}
|
||||
<span>
|
||||
<a class="badge bg-primary tag" href="{% url 'sites:tag' tag.tag_slug %}">{{ tag.tag|capfirst }}</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">This submission has no tags</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr class="m-2">
|
||||
|
||||
<table class="table table-sm rounded-2 bg-secondary">
|
||||
<tbody class="rounded-2">
|
||||
<tr>
|
||||
{% if submission.category.name == "furaffinity" %}
|
||||
<th scope="row" class="text-info">FurAffinity ID</th>
|
||||
{% elif submission.category.name == "twitter" %}
|
||||
<th scope="row" class="text-info">Twitter ID</th>
|
||||
{% elif submission.category.name == "instagram" %}
|
||||
<th scope="row" class="text-info">Instagram ID</th>
|
||||
{% else %}
|
||||
<th scope="row" class="text-info">Submission ID</th>
|
||||
{% endif %}
|
||||
<td>{{ submission.content_object.submission_id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% if submission.category.name == "furaffinity" %}
|
||||
<th scope="row" class="text-info">Views</th>
|
||||
<td>{{ submission.content_object.views }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Gallery Type</th>
|
||||
<td>{{ submission.content_object.gallery_type|capfirst }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Lang</th>
|
||||
<td>{{ submission.content_object.lang }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Favorites</th>
|
||||
<td>{{ submission.content_object.favorites_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Retweets</th>
|
||||
<td>{{ submission.content_object.retweet_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Quotes</th>
|
||||
<td>{{ submission.content_object.quote_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Replies</th>
|
||||
<td>{{ submission.content_object.reply_count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h1 class="text-center m-0">Media Info:</h1>
|
||||
|
||||
<hr class="m-1">
|
||||
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
{% if submission.content_object.files.all|length == 0 %}
|
||||
<tr>
|
||||
<th scope="row" class="text-warning">No Media</th>
|
||||
</tr>
|
||||
{% elif submission.content_object.files.all|length <= 1 %}
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Image Size</th>
|
||||
<td>{{ submission.content_object.files.first.image_width }} x {{ submission.content_object.files.first.image_height }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Size</th>
|
||||
<td>{{ submission.content_object.files.first.size|size_to_human_readable }}</td>
|
||||
</tr>
|
||||
|
||||
|
||||
{% else %}
|
||||
{% for file in submission.content_object.files.all %}
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Image Res {{ forloop.counter }}</th>
|
||||
<td>{{ file.image_width }} x {{ file.image_height }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Size {{ forloop.counter }}</th>
|
||||
<td>{{ file.size|size_to_human_readable }}</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Mature</th>
|
||||
<td><span class="badge {% if submission.content_object.sensitive %}bg-danger{% else %}bg-success{% endif %} text-2xl">{{ submission.content_object.sensitive|default_if_none:False }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Orginal Date</th>
|
||||
<td>{{ submission.content_object.date |date:'Y-m-d H:i:s' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-info">Archive Date</th>
|
||||
<td>{{ submission.date_added |date:'Y-m-d H:i:s' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
{% load static %}
|
||||
|
||||
<a class="site-btn-overlay" href='{% url "sites:site_overview" category %}'>
|
||||
{% if category == "twitter" %}
|
||||
<img src='{% static "/img/site-logos/twitter_logo.png" %}' alt="{{ category }}"/>
|
||||
|
||||
{% elif category == "furaffinity" %}
|
||||
<img src='{% static "/img/site-logos/fa_logo.png" %}' alt="{{ category }}"/>
|
||||
|
||||
{% else %}
|
||||
<small>{{ category|capfirst }}</small>
|
||||
|
||||
{% endif %}
|
||||
</a>
|
|
@ -1,114 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Sites {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
.textonly{
|
||||
background-color:#222222;
|
||||
border-radius: 5px;
|
||||
width:260px;
|
||||
padding:8px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0px 1px 5px 0px #221133;
|
||||
margin:auto auto 16px auto;
|
||||
border:2px solid #eeeeee;
|
||||
}
|
||||
.text2{
|
||||
background-color:#222222;
|
||||
border-radius: 5px;
|
||||
width:760px;
|
||||
padding:8px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0px 1px 5px 0px #221133;
|
||||
margin:auto auto 16px auto;
|
||||
border:2px solid #eeeeee;
|
||||
}
|
||||
.e-container-border{
|
||||
// width: 90%;
|
||||
padding: 4px;
|
||||
margin: auto;
|
||||
background-color:#222222;
|
||||
border-radius: 25px;
|
||||
background: linear-gradient(180deg, #4b8fca, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
box-shadow: 0px 2px 10px 0px #221133;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.3s;
|
||||
display: flex;
|
||||
}
|
||||
.e-container{
|
||||
overflow:hidden;
|
||||
background-color:#222222;
|
||||
padding:24px;
|
||||
text-align:justify;
|
||||
border-radius: 25px;
|
||||
}
|
||||
</style>
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="e-container-border row" tabindex="1">
|
||||
<div class="e-container">
|
||||
<h1 class="text-center">Archived Sites</h1>
|
||||
|
||||
<p class="text-center">These are the sites that have been archived to this archive.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="text-center">Some basic archive stats.</p>
|
||||
|
||||
<p class="text-center">
|
||||
<span>Submissions: {{ basic_stats.submissions }}</span>
|
||||
|
||||
<span>Users: {{ basic_stats.users }}</span>
|
||||
|
||||
<span>Tags: {{ basic_stats.tags }}</span>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<div class="d-flex flex-wrap justify-content-center gap-2">
|
||||
|
||||
{% for site in sites %}
|
||||
<div class="col-sm-8 col-md-5 col-lg-5 col-xl-4">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-4 d-flex align-items-center justify-content-center">
|
||||
<img src="{% static site.logo %}" width="100%" height="100%" class="img-fluid rounded-start" alt="...">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{ site.name }}</h3>
|
||||
<ul>
|
||||
<li>Posts: {{ site.posts }}</li>
|
||||
<li>Users: {{ site.users }}</li>
|
||||
<li>Tags: {{ site.tags }}</li>
|
||||
</ul>
|
||||
<p class="card-text"><small class="text-body-secondary">{{ site.last_updated }}</small></p>
|
||||
{% if site.url %}
|
||||
<a href="{% url 'sites:site_overview' site.url %}" class="stretched-link"></a>
|
||||
{% else %}
|
||||
<a href="#" class="stretched-link" aria-disabled="true"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% load media_filters %}
|
||||
|
||||
{% load string_helper %}
|
||||
|
||||
{% block title %}
|
||||
{% if submission.content_object.title %}
|
||||
{{ submission.content_object.title }}
|
||||
{% else %}
|
||||
{% if submission.content_object.description|length > 16 %}
|
||||
{{ submission.content_object.description|slice:"0:16"|add:"..." }}
|
||||
{% else %}
|
||||
{{ submission.content_object.description }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
by {{ submission.content_object.author.artist }}
|
||||
{% if submission.category.name == "twitter" %}
|
||||
(@{{ submission.content_object.author.artist_url }}) from Twitter
|
||||
|
||||
{% elif submission.category.name == "furaffinity" %}
|
||||
{{ submission.content_object.artist }} from FurAffinity
|
||||
{% endif %}
|
||||
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row row-gap-3 column-gap-0">
|
||||
|
||||
<div class="col-xl-9 col-lg-8 pe-lg-0">
|
||||
<div class="e-container-border e-container-radius">
|
||||
<div id="submission_container" class="e-container e-container-radius bg-black d-flex justify-content-center align-items-center overflow-hidden">
|
||||
{% if submission.content_object.files.exists %}
|
||||
|
||||
{% if submission.content_object.files.all|length == 1 %}
|
||||
|
||||
{% if submission.content_object.files.first.file_mime|is_flash %}
|
||||
<div id="flash_embed"></div>
|
||||
|
||||
{% elif submission.content_object.files.first.file_mime|is_image %}
|
||||
<img class="img-fluid" width="100%" height="auto"
|
||||
src="{% url 'files:serve_content_file' 'submission' submission.content_object.files.first.file_hash %}"
|
||||
alt="{{ submission.content_object.title }}"/>
|
||||
|
||||
|
||||
{% elif submission.content_object.files.first.file_mime|is_pdf %}
|
||||
<!-- Embed the full PDF.js viewer here -->
|
||||
<iframe id="pdf-js-viewer"
|
||||
src="{% static 'libs/pdfjs-4.7.76-dist/web/viewer.html' %}?file={% url 'files:serve_content_file' 'submission' submission.content_object.files.first.file_hash %}"
|
||||
width="100%" height="100%"
|
||||
class="border-0 m-auto flex-grow-1 p-0">
|
||||
</iframe>
|
||||
|
||||
<!-- Scripts required for PDF.js -->
|
||||
<script src="{% static 'libs/pdfjs-4.7.76-dist/build/pdf.js' %}"></script>
|
||||
<script src="{% static 'libs/pdfjs-4.7.76-dist/web/viewer.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="m-auto center-block bg-body-secondary p-4 rounded-2 border border-3">
|
||||
<h3 class="text-center"><i class="nf nf-md-image_broken p-2 me-2 mb-1"></i>No content</h3>
|
||||
<hr>
|
||||
<p class="text-center">This submission has no media or is missing media.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-lg-4">
|
||||
<div class="d-none d-sm-none d-md-none d-lg-block e-container-border e-container-radius">
|
||||
<div class="e-container e-container-radius p-2">
|
||||
{% include "sites/partials/post-info.html" with submission=submission %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="e-container-border e-container-radius row my-3 mt-lg-3 mt-0" tabindex="1">
|
||||
<div class="e-container e-container-radius p-4">
|
||||
|
||||
{% if submission.content_object.title %}
|
||||
<h3 class="bg-body-tertiary p-2 rounded">{{ submission.content_object.title }}</h3>
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex flex-wrap justify-content-center gap-2">
|
||||
{% if next_submission %}
|
||||
<a class="btn btn-primary" href="{% url 'sites:submission' next_submission %}">Next</a>
|
||||
{% else %}
|
||||
<span class="btn btn-outline-primary disabled text-decoration-line-through">Next</span>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-secondary" href="{% url 'sites:artist_profile' submission.author.user_hash %}">View Artist</a>
|
||||
|
||||
{% if prev_submission %}
|
||||
<a class="btn btn-primary" href="{% url 'sites:submission' prev_submission %}">Prev</a>
|
||||
{% else %}
|
||||
<span class="btn btn-outline-primary disabled text-decoration-line-through">Prev</span>
|
||||
{% endif %}
|
||||
|
||||
{% if submission.category.name == "twitter" %}
|
||||
<a class="btn btn-outline-secondary ms-auto"
|
||||
href="https://twitter.com/{{ submission.content_object.author.artist_url }}/status/{{submission.content_object.submission_id}}"
|
||||
target="_blank">
|
||||
View Source
|
||||
</a>
|
||||
{% elif submission.category.name == "furaffinity" %}
|
||||
<a class="btn btn-outline-secondary ms-auto"
|
||||
href="https://www.furaffinity.net/view/{{submission.content_object.submission_id}}"
|
||||
target="_blank">
|
||||
View Source
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.is_staff or request.user.is_superuser %}
|
||||
<button type="button" class="btn btn-danger"
|
||||
hx-delete="{% url 'sites:submission' submission.submission_hash %}"
|
||||
hx-confirm="Are you sure you want to delete this post?"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
Delete Post
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{% if submission.content_object.description %}
|
||||
{% if submission.category.name == "twitter" %}
|
||||
<p class="lg-px-12">{{ submission.content_object.description|clickable_urls|get_twitter_username_from_str|get_tags_from_str|safe }}</p>
|
||||
{% else %}
|
||||
<p>{{ submission.content_object.description|stylizeDescription|clickable_urls|safe }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>No description</p>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-md-block d-lg-none e-container-border e-container-radius row my-3" tabindex="1">
|
||||
<div class="e-container e-container-radius p-4">
|
||||
{% include "sites/partials/post-info.html" with submission=submission %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
{% if submission.content_object.files.first.file_mime|is_flash or submission.content_object.file != None %}
|
||||
<script>
|
||||
var flash_embed = document.getElementById('flash_embed');
|
||||
|
||||
if (flash_embed) {
|
||||
window.RufflePlayer = window.RufflePlayer || {};
|
||||
window.RufflePlayer.config = {
|
||||
"wmode": "direct",
|
||||
"quality": "high",
|
||||
};
|
||||
|
||||
window.addEventListener("load", (event) => {
|
||||
const submission_container = document.getElementById("submission_container");
|
||||
const ruffle = window.RufflePlayer.newest();
|
||||
const player = ruffle.createPlayer();
|
||||
const container = document.getElementById("flash_embed");
|
||||
container.appendChild(player);
|
||||
|
||||
// Set initial dimensions
|
||||
const flash_width = parseInt("{{ submission.content_object.image_width }}");
|
||||
const flash_height = parseInt("{{ submission.content_object.image_height }}");
|
||||
const aspectRatio = flash_width / flash_height;
|
||||
|
||||
resizeFlashEmbed();
|
||||
|
||||
player.load("{% url 'files:serve_content_file' 'submission' submission.content_object.files.first.file_hash %}")
|
||||
.then(() => {
|
||||
console.info("Ruffle successfully loaded the file");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`Ruffle failed to load the file: ${e}`);
|
||||
});
|
||||
|
||||
function resizeFlashEmbed() {
|
||||
|
||||
const flash_embed_player = flash_embed.firstChild;
|
||||
|
||||
const container_width = submission_container.offsetWidth;
|
||||
const container_height = submission_container.offsetHeight;
|
||||
|
||||
// Adjust width and height based on container while keeping aspect ratio
|
||||
let new_width = container_width;
|
||||
let new_height = new_width / aspectRatio;
|
||||
|
||||
// If the height exceeds the container's height, adjust using height
|
||||
if (new_height > container_height) {
|
||||
new_height = container_height;
|
||||
new_width = new_height * aspectRatio;
|
||||
}
|
||||
|
||||
flash_embed_player.style.width = new_width + 'px';
|
||||
flash_embed_player.style.height = new_height + 'px';
|
||||
}
|
||||
|
||||
// Call the function on window resize as well
|
||||
window.addEventListener('resize', resizeFlashEmbed);
|
||||
window.addEventListener('load', resizeFlashEmbed);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
<script src="{% static 'libs/ruffle-nightly-2023_05_04-web-selfhosted/ruffle.js' %}"></script>
|
||||
{% endblock scripts %}
|
|
@ -1,32 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% load string_helper %}
|
||||
|
||||
{% block title %}
|
||||
Tags
|
||||
{% endblock title %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="e-container-border e-container-radius row m-0 mb-3" tabindex="1">
|
||||
<div class="e-container e-container-radius p-4 overflow-x-hidden">
|
||||
<h1 class="text-center">Tags</h1>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mt-2 text-center">
|
||||
{% for tag in tags %}
|
||||
<a class="badge bg-primary text-decoration-none" href="{% url 'sites:tag' tag.tag_slug %}">{{ tag|capfirst }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,118 +0,0 @@
|
|||
{% extends "layouts/base-electric.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Sites {% endblock title %}
|
||||
|
||||
{% block stylesheets %}{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "includes/navigation-transparent.html" %}
|
||||
|
||||
<div class="container-fluid" loading="lazy">
|
||||
<div class="e-container-border e-container-radius row mb-3" tabindex="1">
|
||||
<div class="e-container e-container-radius p-2">
|
||||
|
||||
<h1 class="text-center">Twitter Overview</h1>
|
||||
|
||||
<p class="text-center"></p>
|
||||
|
||||
|
||||
<h2>New Archived Posts:</h2>
|
||||
<div class="gallery-container">
|
||||
{% for submission in submissions %}
|
||||
<div class="gallery-item bg-dark">
|
||||
{% include "sites/partials/site-btn-overlay.html" with category=submission.category.name %}
|
||||
|
||||
{% if submission.content_object.files.exists %}
|
||||
|
||||
{% if submission.content_object.files.all|length == 1 %}
|
||||
<img src="{% url 'files:serve_content_file' 'submission' submission.content_object.files.first.file_hash %}" class="" alt="{{ media_files.0.name }}">
|
||||
|
||||
{% elif submission.content_object.files.all|length == 2 %}
|
||||
{% for file in submission.content_object.files.all %}
|
||||
<img src="{% url 'files:serve_content_file' 'submission' file.file_hash %}" class="" alt="{{ media_file.name }}">
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
{% for file in submission.content_object.files.all %}
|
||||
<div class="col">
|
||||
<img src="{% url 'files:serve_content_file' 'submission' file.file_hash %}" class="" alt="{{ media_file.name }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
|
||||
<span class="badge bg-secondary">This submission has no media</span>
|
||||
|
||||
{% endif %}
|
||||
<a href='{% url "sites:submission" submission.submission_hash %}' class="stretched-link"></a>
|
||||
<div class="overlay p-2 text-center">
|
||||
{% if submission.content_object.description|length > 64 %}
|
||||
<p>{{ submission.content_object.description|slice:"0:64"|add:"..." }}</p>
|
||||
{% else %}
|
||||
<p>{{ submission.content_object.description }}</p>
|
||||
{% endif %}
|
||||
<a href="{% url 'sites:artist_profile' submission.author.user_hash %}" class="z-2">{{ submission.content_object.author.artist }}</a>
|
||||
<small class="badge bg-secondary">{{ submission.content_object.date }}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="e-container-border row my-3" tabindex="1">
|
||||
<div class="e-container">
|
||||
|
||||
<h2 class="text-center">New Archived Users:</h2>
|
||||
<div class="d-flex overflow-auto">
|
||||
<div class="list-group list-group-horizontal">
|
||||
{% for user in new_users %}
|
||||
<div class="list-group-item bg-transparent border-0">
|
||||
|
||||
<div class="card text-bg-secondary" style="min-width: 48ch;">
|
||||
{% if user.banner %}
|
||||
<img src="{% url 'files:serve_content_file' 'user_banner' user.banner.file_hash %}" class="card-img-top" alt="{{ user.artist }}'s banner" loading="lazy">
|
||||
{% else %}
|
||||
{% comment %} <div class="placeholder" style="padding-top: 33.33333333333333%;"></div> {% endcomment %}
|
||||
<img src="{% static 'img/placeholder/no-banner-1500x500.png' %}" class="card-img-top" alt="{{ user.artist }} has no banner">
|
||||
{% endif %}
|
||||
<div class="row g-0 p-2">
|
||||
<div class="col-4">
|
||||
{% if user.icon %}
|
||||
<img src="{% url 'files:serve_content_file' 'user_profile' user.icon.file_hash %}" class="mt-3 card-img rounded-circle border border-3 border-primary" alt="{{ user.artist }}'s banner" loading="lazy">
|
||||
{% else %}
|
||||
{% comment %} <div class="placeholder" style="padding-top: 33.33333333333333%;"></div> {% endcomment %}
|
||||
<img src="{% static 'img/placeholder/no-icon-500x500.png' %}" class="card-img rounded-circle" alt="{{ user.artist }} has no icon">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{ user.artist }}</h5>
|
||||
<p class="card-text">{{ user.description }}</p>
|
||||
<p class="card-text"><small class="text-body-secondary">Added on: {{ user.date_added }}</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'sites:submission' user.id %}" class="stretched-link"></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import UserProfile, SeenPost
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class UserProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'show_mature')
|
||||
|
||||
class SeenPostAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'post', 'timestamp')
|
||||
|
||||
|
||||
admin.site.register(UserProfile, UserProfileAdmin)
|
||||
admin.site.register(SeenPost, SeenPostAdmin)
|
|
@ -1,13 +0,0 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from .models import UserProfile
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['show_mature', "items_per_page", "post_seen_delay"]
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'email', 'first_name', 'last_name']
|
|
@ -1,34 +0,0 @@
|
|||
# Generated by Django 4.1.1 on 2023-11-01 14:43
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('furaffinity', '0001_initial'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserProfile',
|
||||
fields=[
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('show_mature', models.CharField(choices=[('H', 'Hide'), ('B', 'Blur'), ('S', 'Show')], default='H', max_length=2)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SeenPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='furaffinity.fa_submission')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.userprofile')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,52 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.sites.models import Submissions
|
||||
|
||||
class UserProfile(models.Model):
|
||||
|
||||
MATURE = [
|
||||
("H", "Hide"),
|
||||
("B", "Blur"),
|
||||
("S", "Show"),
|
||||
]
|
||||
|
||||
ITEMS_PER_PAGE = [
|
||||
(24, "24"),
|
||||
(48, "48"),
|
||||
(72, "72"),
|
||||
]
|
||||
|
||||
POST_SEEN_DELAY = [
|
||||
(15, "15"),
|
||||
(30, "30"),
|
||||
(60, "60"),
|
||||
(90, "90"),
|
||||
]
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True, unique=True)
|
||||
show_mature = models.CharField(max_length=2, choices=MATURE, default=MATURE[0][0])
|
||||
items_per_page = models.IntegerField(choices=ITEMS_PER_PAGE, default=ITEMS_PER_PAGE[0][0])
|
||||
post_seen_delay = models.IntegerField(choices=POST_SEEN_DELAY, default=POST_SEEN_DELAY[1][1], help_text="Delay in seconds before marking a post as seen")
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User Profile")
|
||||
verbose_name_plural = _("User Profiles")
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
||||
class SeenPost(models.Model):
|
||||
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
|
||||
post = models.ForeignKey(Submissions, on_delete=models.CASCADE)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Seen Post")
|
||||
verbose_name_plural = _("Seen Posts")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.user.user.username + " - " + self.post.submission_hash
|
|
@ -1,7 +0,0 @@
|
|||
from django.urls import path
|
||||
from .views import ProfileEditView
|
||||
|
||||
urlpatterns = [
|
||||
# Other URL patterns
|
||||
path('profile/edit', ProfileEditView, name='profile'),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
from apps.user.models import UserProfile, SeenPost
|
||||
|
||||
from apps.sites.models import Submissions
|
||||
|
||||
from .forms import UserProfileForm, UserForm
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def ProfileEditView(request):
|
||||
if request.method == 'POST':
|
||||
user_form = UserForm(request.POST, instance=request.user)
|
||||
profile_form = UserProfileForm(request.POST, instance=request.user.userprofile)
|
||||
|
||||
if user_form.is_valid() and profile_form.is_valid():
|
||||
user_form.save()
|
||||
profile_form.save()
|
||||
# Redirect to a success page or home page
|
||||
else:
|
||||
user_form = UserForm(instance=request.user)
|
||||
profile_form = UserProfileForm(instance=request.user.userprofile)
|
||||
|
||||
return render(request, 'accounts/profile.html', {'user_form': user_form, 'profile_form': profile_form})
|
|
@ -1,10 +0,0 @@
|
|||
# core/__init__.py
|
||||
|
||||
# Import the Celery app instance from the celery.py file in the same directory (core/celery.py).
|
||||
# The celery.py file is where the Celery app instance is created and configured.
|
||||
from .celery import app as celery_app
|
||||
|
||||
# The __all__ variable is used to define what should be exported when someone imports the package (core).
|
||||
# By specifying __all__ = ("celery_app",), we are explicitly stating that the celery_app should be exported
|
||||
# when someone imports the core package.
|
||||
__all__ = ("celery_app",)
|
|
@ -1,25 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
# Set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
|
||||
app = Celery('archivist')
|
||||
|
||||
# Using a string here means the worker doesn't have to serialize
|
||||
# the configuration object to child processes.
|
||||
# - namespace='CELERY' means all celery-related configuration keys
|
||||
# should have a `CELERY_` prefix.
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
app.autodiscover_tasks()
|
||||
|
||||
@app.task(bind=True)
|
||||
def debug_task(self):
|
||||
print('Request: {0!r}'.format(self.request))
|
||||
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
"""
|
||||
Django settings for archivist project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.0.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os, environ
|
||||
|
||||
env = environ.Env(
|
||||
# set casting, default value
|
||||
DEBUG=(bool, False)
|
||||
)
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
CORE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Take environment variables from .env file
|
||||
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env('SECRET_KEY', default='S#perS3crEt_007')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env('DEBUG')
|
||||
|
||||
# Assets Management
|
||||
ASSETS_ROOT = os.getenv('ASSETS_ROOT', '/static/assets')
|
||||
|
||||
# load production server from .env
|
||||
ALLOWED_HOSTS = ['localhost', 'localhost:85', '127.0.0.1', env('SERVER', default='127.0.0.1') ]
|
||||
CSRF_TRUSTED_ORIGINS = ['http://localhost:85', 'http://127.0.0.1', 'https://' + env('SERVER', default='127.0.0.1') ]
|
||||
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# Django core apps
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Pip module libraries
|
||||
'whitenoise.runserver_nostatic',
|
||||
'rest_framework',
|
||||
'sorl.thumbnail',
|
||||
|
||||
'django_cleanup.apps.CleanupConfig',
|
||||
|
||||
"django_celery_beat",
|
||||
"django_celery_results",
|
||||
|
||||
'apps.files',
|
||||
|
||||
# Gallery Archivist apps
|
||||
'sites.furaffinity'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'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 = 'core.urls'
|
||||
TEMPLATE_DIR_APPS = os.path.join(CORE_DIR, "apps/templates") # ROOT dir for templates
|
||||
TEMPLATE_DIR_SITES = os.path.join(CORE_DIR, "sites/templates")
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [TEMPLATE_DIR_APPS, TEMPLATE_DIR_SITES, "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',
|
||||
'apps.context_processors.debug',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'core.wsgi.application'
|
||||
|
||||
#############################################################
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
#############################################################
|
||||
if os.environ.get('DB_ENGINE') and os.environ.get('DB_ENGINE') == "mysql":
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE' : 'django.db.backends.mysql',
|
||||
'NAME' : os.getenv('DB_NAME' , 'gallery_archivist_db'),
|
||||
'USER' : os.getenv('DB_USERNAME' , 'gallery_archivist_db_usr'),
|
||||
'PASSWORD': os.getenv('DB_PASS' , 'pass'),
|
||||
'HOST' : os.getenv('DB_HOST' , 'localhost'),
|
||||
'PORT' : os.getenv('DB_PORT' , 3306),
|
||||
},
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
#############################################################
|
||||
|
||||
|
||||
#############################################################
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/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.0/topics/i18n/
|
||||
#############################################################
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
#############################################################
|
||||
|
||||
|
||||
#############################################################
|
||||
# SRC: https://devcenter.heroku.com/articles/django-assets
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||
#############################################################
|
||||
STATIC_ROOT = os.path.join(CORE_DIR, 'staticfiles')
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Extra places for collectstatic to find static files.
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(CORE_DIR, 'apps/static'),
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
)
|
||||
|
||||
#STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
#############################################################
|
||||
|
||||
|
||||
#############################################################
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
#############################################################
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
#############################################################
|
||||
|
||||
|
||||
#############################################################
|
||||
# Celery
|
||||
#############################################################
|
||||
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER", 'redis://localhost:6379/0')
|
||||
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
|
||||
#CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
|
||||
|
||||
CELERY_RESULT_BACKEND = 'django-db'
|
||||
CELERY_CACHE_BACKEND = 'django-cache'
|
||||
|
||||
CELERY_RESULT_EXTENDED = True
|
||||
|
||||
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
||||
#############################################################
|
||||
|
||||
|
||||
# Allow embedding in iframes from the same origin
|
||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
|
@ -1,79 +0,0 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
|
||||
{% block extrahead %}{{ block.super }}
|
||||
<style>
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
:root {
|
||||
--primary: #62095c; {% comment %} #9106c4; {% endcomment %}
|
||||
--primary-fg: #eee;
|
||||
--body-fg: #eeeeee;
|
||||
--body-bg: #040b1e;
|
||||
--body-quiet-color: #e0e0e0;
|
||||
--body-loud-color: #ffffff;
|
||||
--breadcrumbs-link-fg: #e0e0e0;
|
||||
--breadcrumbs-bg: var(--primary);
|
||||
--link-fg: #81d4fa;
|
||||
--link-hover-color: #4ac1f7;
|
||||
--link-selected-fg: #6f94c6;
|
||||
--hairline-color: #272727;
|
||||
--border-color: #353535;
|
||||
--error-fg: #e35f5f;
|
||||
--message-success-bg: #006b1b;
|
||||
--message-warning-bg: #583305;
|
||||
--message-error-bg: #570808;
|
||||
--darkened-bg: #0f1e31;
|
||||
--selected-bg: #1b1b1b;
|
||||
--selected-row: #00363a;
|
||||
--close-button-bg: #333333;
|
||||
--close-button-hover-bg: #666666;
|
||||
|
||||
--header-color: #fff;
|
||||
--header-bg: #9106c4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
|
||||
:root {
|
||||
--primary: #79aec8;
|
||||
--secondary: #417690;
|
||||
--accent: #f5dd5d;
|
||||
--primary-fg: #fff;
|
||||
--body-fg: #333;
|
||||
--body-bg: #fff;
|
||||
--body-quiet-color: #666;
|
||||
--body-loud-color: #000;
|
||||
--header-color: #ffc;
|
||||
--header-branding-color: var(--accent);
|
||||
--header-bg: var(--secondary);
|
||||
--header-link-color: var(--primary-fg);
|
||||
--breadcrumbs-fg: #c4dce8;
|
||||
--breadcrumbs-link-fg: var(--body-bg);
|
||||
--breadcrumbs-bg: var(--primary);
|
||||
--link-fg: #447e9b;
|
||||
--link-hover-color: #036;
|
||||
--link-selected-fg: #5b80b2;
|
||||
--hairline-color: #e8e8e8;
|
||||
--border-color: #ccc;
|
||||
--error-fg: #ba2121;
|
||||
--message-success-bg: #dfd;
|
||||
--message-warning-bg: #ffc;
|
||||
--message-error-bg: #ffefef;
|
||||
--darkened-bg: #f8f8f8;
|
||||
--selected-bg: #e4e4e4;
|
||||
--selected-row: #ffc;
|
||||
--button-fg: #fff;
|
||||
--button-bg: var(--primary);
|
||||
--button-hover-bg: #609ab6;
|
||||
--default-button-bg: var(--secondary);
|
||||
--default-button-hover-bg: #205067;
|
||||
--close-button-bg: #888;
|
||||
--close-button-hover-bg: #747474;
|
||||
--delete-button-bg: #ba2121;
|
||||
--delete-button-hover-bg: #a41515;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
|
@ -1,2 +0,0 @@
|
|||
# __init__.py
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import FA_Submission, FA_Submission_File, FA_Tags, FA_User, FA_Species, FA_Gender
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class FA_UserAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
("Artist Name", { "fields": ["artist", "artist_url"], } ),
|
||||
)
|
||||
|
||||
|
||||
class FA_TagsAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.display(description='Tag Name')
|
||||
def upper_case_tag(obj):
|
||||
return ("%s" % (obj.tag).capitalize())
|
||||
|
||||
list_display = (upper_case_tag,)
|
||||
|
||||
|
||||
class FA_SubmissionAdmin(admin.ModelAdmin):
|
||||
list_display = ('submission_id', 'title', 'artist', 'date', 'date_added', 'mature_rating',)
|
||||
|
||||
class FA_Submission_FileAdmin(admin.ModelAdmin):
|
||||
list_display = ('file_name', 'date_added', 'file_hash', 'file',)
|
||||
|
||||
admin.site.register(FA_User, FA_UserAdmin,)
|
||||
admin.site.register(FA_Tags, FA_TagsAdmin,)
|
||||
admin.site.register(FA_Species,)
|
||||
admin.site.register(FA_Gender,)
|
||||
admin.site.register(FA_Submission, FA_SubmissionAdmin,)
|
||||
admin.site.register(FA_Submission_File, FA_Submission_FileAdmin,)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FuraffinityConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'sites.furaffinity'
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MyConfig(AppConfig):
|
||||
name = 'sites.furaffinity'
|
||||
label = 'sites_furaffinity'
|
|
@ -1,15 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
from .models import FA_Submission
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
search_query = forms.CharField(label='Search', max_length=100)
|
||||
|
||||
class URLImportForm(forms.Form):
|
||||
url = forms.URLField(label='Post URL', required=True)
|
||||
|
||||
|
||||
#class DateTimeForm(forms.ModelForm):
|
||||
# class Meta:
|
||||
# model = FA_Submission
|
||||
# fields = ['submission_id', 'media_url', 'title', 'description', 'artist', 'date', 'species']
|
|
@ -1,186 +0,0 @@
|
|||
# your_app/management/commands/import_data.py
|
||||
|
||||
import os
|
||||
import json
|
||||
from blake3 import blake3
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
#from PIL import Image
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.text import slugify
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
#from django.core.files import File
|
||||
from django.utils import timezone
|
||||
|
||||
from sites.furaffinity.models import FA_Submission, FA_Submission_File, FA_Tags, FA_User, FA_Species, FA_Gender, FA_Mature
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Import data from JSON files in a folder or a single JSON file to the furaffinity archive'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('path', type=str, help='Path to the folder containing JSON files or a single JSON file')
|
||||
parser.add_argument('--delete', action='store_true', help='Delete imported files')
|
||||
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
path = kwargs['path']
|
||||
delete = kwargs['delete']
|
||||
|
||||
if os.path.isfile(path):
|
||||
self.process_json_file(path, delete)
|
||||
elif os.path.isdir(path):
|
||||
self.process_json_folder(path, delete)
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f"The path '{path}' is not a valid file or folder."))
|
||||
return
|
||||
|
||||
|
||||
def process_json_file(self, file_path, delete):
|
||||
#self.stdout.write(self.style.NOTICE(f"Importing data from: {file_path}"))
|
||||
tqdm.write(self.style.NOTICE(f"Importing data from: {file_path}"))
|
||||
|
||||
with open(file_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.import_data(data, file_path, delete)
|
||||
|
||||
#self.stdout.write(self.style.SUCCESS('Data imported successfully.'))
|
||||
tqdm.write(self.style.SUCCESS('Data imported successfully.'))
|
||||
|
||||
def process_json_folder(self, folder_path, delete):
|
||||
if not os.path.exists(folder_path):
|
||||
#self.stdout.write(self.style.ERROR(f"The folder '{folder_path}' does not exist."))
|
||||
tqdm.write(self.style.ERROR(f"The folder '{folder_path}' does not exist."))
|
||||
return
|
||||
|
||||
for root, dirs, files in tqdm(os.walk(folder_path), dynamic_ncols=True):
|
||||
for file_name in files:
|
||||
if file_name.endswith('.json'):
|
||||
file_path = os.path.join(root, file_name)
|
||||
self.process_json_file(file_path, delete)
|
||||
|
||||
|
||||
|
||||
def import_data(self, data, json_file_path, delete):
|
||||
#self.stdout.write(self.style.NOTICE(data))
|
||||
|
||||
submission, created = FA_Submission.objects.get_or_create(submission_id=data["id"])
|
||||
|
||||
submission.media_url = data["url"]
|
||||
submission.title = data["title"]
|
||||
submission.description = data["description"]
|
||||
|
||||
submission.date = timezone.make_aware(datetime.strptime(data["date"], "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
artist, created = FA_User.objects.get_or_create(artist_url=data["artist_url"], artist=data["artist"])
|
||||
submission.artist = artist
|
||||
|
||||
species, created = FA_Species.objects.get_or_create(species=data["species"])
|
||||
submission.species = species
|
||||
|
||||
for tag_name in data["tags"]:
|
||||
tag_slug = slugify(tag_name)
|
||||
try:
|
||||
# Check if the tag already exists in the database by name
|
||||
tag = FA_Tags.objects.get(tag_slug=tag_slug)
|
||||
except ObjectDoesNotExist:
|
||||
# If the tag does not exist, create a new tag and generate the slug
|
||||
tag = FA_Tags(tag=tag_name)
|
||||
tag.tag_slug = tag_slug
|
||||
|
||||
tag.save() # Save the tag (either new or existing)
|
||||
|
||||
submission.tags.add(tag) # Add the tag to the submission
|
||||
|
||||
mature, created = FA_Mature.objects.get_or_create(mature=data["rating"])
|
||||
submission.mature_rating = mature
|
||||
|
||||
submission.number_of_comments = data["comments"]
|
||||
submission.views = data["views"]
|
||||
|
||||
gender, created = FA_Gender.objects.get_or_create(gender=data["gender"])
|
||||
submission.gender = gender
|
||||
|
||||
submission.fa_theme = data["theme"]
|
||||
submission.fa_category = data["fa_category"]
|
||||
submission.gallery_type = data["subcategory"]
|
||||
submission.file_extension = data["extension"]
|
||||
submission.image_height = data["height"]
|
||||
submission.image_width = data["width"]
|
||||
|
||||
|
||||
file_path = json_file_path.removesuffix(".json")
|
||||
|
||||
# Handle file import
|
||||
if os.path.exists(file_path):
|
||||
file_hash = self.compute_file_hash(file_path)
|
||||
try:
|
||||
file_instance = FA_Submission_File.objects.get(file_hash=file_hash)
|
||||
#self.stdout.write(self.style.NOTICE(f"Skipping: {file_path} file, already imported"))
|
||||
tqdm.write(self.style.NOTICE(f"Skipping: {file_path} file, already imported"))
|
||||
except FA_Submission_File.DoesNotExist:
|
||||
# If the file doesn't exist, create a new file instance and link it to the submission
|
||||
with open(file_path, 'rb') as file:
|
||||
file_instance = FA_Submission_File()
|
||||
file_instance.file_hash = file_hash
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
Null, file_ext = os.path.splitext(file_name)
|
||||
hash_file_name = file_hash + file_ext
|
||||
file_instance.file.save(hash_file_name, file)
|
||||
|
||||
file_instance.file_name = file_name
|
||||
file_instance.save()
|
||||
|
||||
tqdm.write(self.style.NOTICE(f"Import media file: {file_path}"))
|
||||
|
||||
# Now link the image_instance to your_model_instance
|
||||
submission.file = file_instance
|
||||
|
||||
|
||||
else:
|
||||
#self.stdout.write(self.style.WARNING(f"File not found: {file_path}"))
|
||||
tqdm.write(self.style.WARNING(f"File not found: {file_path}"))
|
||||
#file, created = FA_Submission_File.objects.get_or_create(file=file_hash)
|
||||
#print(file)
|
||||
#if not FA_Submission.objects.filter(image=image_hash).exists():
|
||||
# with open(image_file_path, 'rb') as img_file:
|
||||
# submission.image.save(os.path.basename(image_file_path), File(img_file), save=True)
|
||||
# submission.image_hash = image_hash # Save the image hash in the model
|
||||
#else:
|
||||
# self.stdout.write(self.style.WARNING(f"Skipping duplicate image: {image_file_path}"))
|
||||
|
||||
#if os.path.exists(image_file_path):
|
||||
# with open(image_file_path, 'rb') as img_file:
|
||||
# submission.image.save(os.path.basename(image_file_path), File(img_file), save=True)
|
||||
|
||||
submission.save()
|
||||
|
||||
self.delete_imported_file(json_file_path, delete)
|
||||
self.delete_imported_file(file_path, delete)
|
||||
|
||||
|
||||
def compute_file_hash(self, file_path):
|
||||
try:
|
||||
# Compute BLAKE3 hash of the file
|
||||
hasher = blake3()
|
||||
with open(file_path, 'rb') as f:
|
||||
while chunk := f.read(65536):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
except Exception as e:
|
||||
#self.stdout.write(self.style.WARNING(f"Error computing file hash: {e}"))
|
||||
tqdm.write(self.style.WARNING(f"Error computing file hash: {e}"))
|
||||
return None
|
||||
|
||||
|
||||
# Delete the file if the --delete flag is used
|
||||
def delete_imported_file(self, file_path, delete):
|
||||
if delete:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
tqdm.write(self.style.SUCCESS(f"Deleted: {file_path}"))
|
||||
else:
|
||||
tqdm.write(self.style.WARNING(f"File not found: {file_path}"))
|
|
@ -1,131 +0,0 @@
|
|||
# Generated by Django 4.1.1 on 2023-10-21 16:51
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FA_Gender',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('gender', models.CharField(max_length=30, unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Gender',
|
||||
'verbose_name_plural': 'Genders',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FA_Mature',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mature', models.CharField(max_length=30, unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Mature Rating',
|
||||
'verbose_name_plural': 'Mature Ratings',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FA_Species',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('species', models.CharField(max_length=50, unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Species',
|
||||
'verbose_name_plural': 'Species',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FA_Submission_File',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file_hash', models.CharField(max_length=64, unique=True)),
|
||||
('file_name', models.CharField(blank=True, max_length=150)),
|
||||
('file', models.FileField(blank=True, upload_to='furaffinity/submissions/')),
|
||||
('date_added', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Submission File',
|
||||
'verbose_name_plural': 'Submission Files',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FA_Tags',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tag', models.CharField(max_length=250, unique=True, verbose_name='Tag name')),
|
||||
('tag_slug', models.SlugField(max_length=260, unique=True, verbose_name='Tag slug/url')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Tag',
|
||||
'verbose_name_plural': 'Tags',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FA_UserIconFile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('icon_file_hash', models.CharField(max_length=64, unique=True)),
|
||||
('icon_file_name', models.CharField(blank=True, max_length=150)),
|
||||
('icon_file', models.FileField(blank=True, upload_to='furaffinity/user_icon/')),
|
||||
('date_added', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User Icon File',
|
||||
'verbose_name_plural': 'User Icon Files',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FA_User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('artist', models.CharField(max_length=35)),
|
||||
('artist_url', models.SlugField(max_length=40, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('icon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='furaffinity.fa_usericonfile')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User',
|
||||
'verbose_name_plural': 'Users',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FA_Submission',
|
||||
fields=[
|
||||
('submission_id', models.PositiveBigIntegerField(default=1, primary_key=True, serialize=False, unique=True, verbose_name='ID')),
|
||||
('media_url', models.URLField(blank=True)),
|
||||
('title', models.CharField(default='', max_length=60)),
|
||||
('description', models.TextField(blank=True, default='', null=True)),
|
||||
('date', models.DateTimeField(null=True)),
|
||||
('date_added', models.DateTimeField(auto_now_add=True)),
|
||||
('number_of_comments', models.PositiveIntegerField(null=True)),
|
||||
('views', models.PositiveIntegerField(null=True)),
|
||||
('fa_theme', models.CharField(max_length=50, null=True, verbose_name='FA Theme')),
|
||||
('fa_category', models.CharField(max_length=50, null=True, verbose_name='FA Category')),
|
||||
('gallery_type', models.CharField(blank=True, max_length=20)),
|
||||
('file_extension', models.CharField(blank=True, max_length=10)),
|
||||
('image_height', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('image_width', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('artist', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='furaffinity.fa_user')),
|
||||
('file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='furaffinity.fa_submission_file')),
|
||||
('gender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='furaffinity.fa_gender')),
|
||||
('mature_rating', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='furaffinity.fa_mature')),
|
||||
('species', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='furaffinity.fa_species')),
|
||||
('tags', models.ManyToManyField(to='furaffinity.fa_tags')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Submission',
|
||||
'verbose_name_plural': 'Submissions',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,222 +0,0 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.text import slugify
|
||||
from django.urls import reverse
|
||||
|
||||
from django.db import models
|
||||
|
||||
import os
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class FA_UserIconFile(models.Model):
|
||||
|
||||
icon_file_hash = models.CharField(unique=True, max_length=64,)
|
||||
icon_file_name = models.CharField(max_length=150, blank=True)
|
||||
icon_file = models.FileField(upload_to="furaffinity/user_icon/", blank=True)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User Icon File")
|
||||
verbose_name_plural = _("User Icon Files")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
#def get_absolute_url(self):
|
||||
# return reverse("FA_UserIconFile_detail", kwargs={"pk": self.pk})
|
||||
|
||||
|
||||
class FA_User(models.Model):
|
||||
|
||||
artist = models.CharField(max_length=35, unique=False,)
|
||||
artist_url = models.SlugField(max_length=40, unique=True,)
|
||||
description = models.TextField(blank=True)
|
||||
icon = models.ForeignKey(FA_UserIconFile, on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return self.word
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
|
||||
def __str__(self):
|
||||
return self.artist
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"/fa/user/{self.artist_url}"
|
||||
#return reverse("fa_user", kwargs={"pk": self.pk})
|
||||
|
||||
|
||||
class FA_Tags(models.Model):
|
||||
|
||||
tag = models.CharField(verbose_name="Tag name", max_length=250, unique=True)
|
||||
tag_slug = models.SlugField(verbose_name="Tag slug/url", max_length=260, unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Tag")
|
||||
verbose_name_plural = _("Tags")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# add slug if not already exists on save.
|
||||
if not self.tag_slug:
|
||||
self.tag_slug = slugify(self.tag)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.tag
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f"/fa/tag/{self.tag_slug}"
|
||||
|
||||
# def get_absolute_url(self):
|
||||
# return f"/fa/tag/{self.tag_url}"
|
||||
# return reverse("FA_Tag_detail", kwargs={"pk": self.pk})
|
||||
|
||||
class FA_Species(models.Model):
|
||||
|
||||
species = models.CharField(unique=True, max_length=50)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Species")
|
||||
verbose_name_plural = _("Species")
|
||||
|
||||
def __str__(self):
|
||||
return self.species
|
||||
|
||||
# def get_absolute_url(self):
|
||||
# return reverse("FA_Species_detail", kwargs={"pk": self.pk})
|
||||
|
||||
class FA_Gender(models.Model):
|
||||
|
||||
gender = models.CharField(unique=True, max_length=30)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Gender")
|
||||
verbose_name_plural = _("Genders")
|
||||
|
||||
def __str__(self):
|
||||
return self.gender
|
||||
|
||||
# def get_absolute_url(self):
|
||||
# return reverse("FA_Gender_detail", kwargs={"pk": self.pk})
|
||||
|
||||
class FA_Mature(models.Model):
|
||||
|
||||
mature = models.CharField(max_length=30, unique=True) #verbose_name="Mature Rating",
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Mature Rating")
|
||||
verbose_name_plural = _("Mature Ratings")
|
||||
|
||||
def __str__(self):
|
||||
return self.mature
|
||||
|
||||
#def get_absolute_url(self):
|
||||
# return reverse("FA_Mature_detail", kwargs={"pk": self.pk})
|
||||
|
||||
class FA_Submission_File(models.Model):
|
||||
|
||||
file_hash = models.CharField(unique=True, max_length=64,)
|
||||
file_name = models.CharField(max_length=150, blank=True)
|
||||
file = models.FileField(upload_to="furaffinity/submissions/", blank=True)
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Submission File")
|
||||
verbose_name_plural = _("Submission Files")
|
||||
|
||||
def __str__(self):
|
||||
return self.file_hash
|
||||
|
||||
#def delete(self, *args, **kwargs):
|
||||
# # Delete the file from the disk when the model instance is deleted
|
||||
# print('Deleting image:', self.file)
|
||||
# if self.file:
|
||||
# if os.path.isfile(self.file.path):
|
||||
# os.remove(self.file.path)
|
||||
#
|
||||
# super().delete(*args, **kwargs)
|
||||
|
||||
#def get_absolute_url(self):
|
||||
# return reverse("Submission_Image_detail", kwargs={"pk": self.pk})
|
||||
|
||||
|
||||
|
||||
class FA_Submission(models.Model):
|
||||
|
||||
MATURE = [
|
||||
("G", "General"),
|
||||
("M", "Mature"),
|
||||
("A", "Adult"),
|
||||
]
|
||||
|
||||
GENDER = [
|
||||
("Ma", "Male"),
|
||||
("Fe", "Female"),
|
||||
("He", "Herm"),
|
||||
("In", "Intersex"),
|
||||
("TM", "Trans (Male)"),
|
||||
("TF", "Trans (Female)"),
|
||||
("NB", "Non-Binary"),
|
||||
("MC", "Multiple characters"),
|
||||
("ON", "Other / Not Specified"),
|
||||
]
|
||||
|
||||
# id
|
||||
submission_id = models.PositiveBigIntegerField(verbose_name="ID", primary_key=True, unique=True, default=1)
|
||||
# url
|
||||
media_url = models.URLField(blank=True)
|
||||
# title
|
||||
title = models.CharField(default="", max_length=60)
|
||||
# description
|
||||
description = models.TextField(default="", null=True, blank=True)
|
||||
# artist
|
||||
artist = models.ForeignKey(FA_User, on_delete=models.CASCADE, null=True, related_name='submissions')
|
||||
# date
|
||||
date = models.DateTimeField(null=True)
|
||||
# NOT imported
|
||||
date_added = models.DateTimeField(auto_now_add=True, editable=True)
|
||||
# species
|
||||
species = models.ForeignKey(FA_Species, on_delete=models.CASCADE, null=True)
|
||||
# tags
|
||||
tags = models.ManyToManyField(FA_Tags)
|
||||
# rating
|
||||
mature_rating = models.ForeignKey(FA_Mature, on_delete=models.CASCADE, null=True) #CharField(max_length=2, choices=MATURE, default=MATURE[0][0])
|
||||
# comments
|
||||
number_of_comments = models.PositiveIntegerField(null=True)
|
||||
# views
|
||||
views = models.PositiveIntegerField(null=True)
|
||||
# gender
|
||||
gender = models.ForeignKey(FA_Gender, on_delete=models.CASCADE, null=True)
|
||||
# theme
|
||||
fa_theme = models.CharField(verbose_name="FA Theme", max_length=50, null=True)
|
||||
# fa_category
|
||||
fa_category = models.CharField(verbose_name="FA Category", max_length=50, null=True)
|
||||
# subcategory
|
||||
gallery_type = models.CharField(max_length=20, blank=True)
|
||||
# extension
|
||||
file_extension = models.CharField(max_length=10, blank=True)
|
||||
# height
|
||||
image_height = models.PositiveIntegerField(blank=True, null=True)
|
||||
# width
|
||||
image_width = models.PositiveIntegerField(blank=True, null=True)
|
||||
# ImageField for the image files
|
||||
file = models.ForeignKey(FA_Submission_File, on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Submission")
|
||||
verbose_name_plural = _("Submissions")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
#def save(self, *args, **kwargs):
|
||||
# if not self.related_model:
|
||||
# self.related_model, created = FA_User.objects.get_or_create(name=self.artist)
|
||||
# super().save(*args, **kwargs)
|
||||
|
||||
# def get_absolute_url(self):
|
||||
# return reverse("FA_Submission_detail", kwargs={"pk": self.pk})
|
|
@ -1,14 +0,0 @@
|
|||
from django.contrib.auth.models import User, Group
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['url', 'username', 'email', 'groups']
|
||||
|
||||
|
||||
class GroupSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = ['url', 'name']
|
|
@ -1,153 +0,0 @@
|
|||
#tasks.py
|
||||
import requests
|
||||
import subprocess, os
|
||||
|
||||
from sys import stdout, stderr
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from blake3 import blake3
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from .models import FA_User, FA_UserIconFile
|
||||
|
||||
@shared_task
|
||||
def fa_import_data():
|
||||
try:
|
||||
# Get the current working directory
|
||||
# current_dir = os.getcwd()
|
||||
|
||||
# Change the working directory to the parent folder
|
||||
# os.chdir(os.path.dirname(current_dir))
|
||||
|
||||
result = subprocess.run(["python", "manage.py", "import_data", "gallery-dl/", "--delete"], capture_output=True, text=True)
|
||||
|
||||
return {
|
||||
'stdout': result.stdout,
|
||||
'stderr': result.stderr,
|
||||
'returncode': result.returncode
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
@shared_task
|
||||
def scrape_fa_submission(url):
|
||||
# print(url)
|
||||
# print(subprocess.run(['pwd'], capture_output=True, text=True))
|
||||
try:
|
||||
# Get the current working directory
|
||||
# current_dir = os.getcwd()
|
||||
|
||||
# Change the working directory to the parent folder
|
||||
# os.chdir(os.path.dirname(current_dir))
|
||||
|
||||
|
||||
result = subprocess.run(['gallery-dl', '-c','../gallery-dl.conf', '-d', 'gallery-dl', '--mtime-from-date', '--write-metadata', url], capture_output=True, text=True)
|
||||
|
||||
importTask = fa_import_data.delay()
|
||||
print(importTask)
|
||||
|
||||
return {
|
||||
'stdout': result.stdout,
|
||||
'stderr': result.stderr,
|
||||
'returncode': result.returncode
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
@shared_task
|
||||
def get_fa_user_info(user):
|
||||
|
||||
#try:
|
||||
# file_instance = FA_UserIconFile.objects.get(file_hash=file_hash)
|
||||
# #self.stdout.write(self.style.NOTICE(f"Skipping: {file_path} file, already imported"))
|
||||
# print(f"Skipping: {file_path} file, already imported")
|
||||
#except FA_UserIconFile.DoesNotExist:
|
||||
# # If the file doesn't exist, create a new file instance and link it to the submission
|
||||
# with open(file_path, 'rb') as file:
|
||||
# file_instance = FA_Submission_File()
|
||||
# file_instance.file_hash = file_hash
|
||||
#
|
||||
# file_name = os.path.basename(file_path)
|
||||
# Null, file_ext = os.path.splitext(file_name)
|
||||
# hash_file_name = file_hash + file_ext
|
||||
# file_instance.file.save(hash_file_name, file)
|
||||
#
|
||||
# file_instance.file_name = file_name
|
||||
# file_instance.save()
|
||||
|
||||
# # Now link the image_instance to your_model_instance
|
||||
# submission.file = file_instance
|
||||
|
||||
url = "https://www.furaffinity.net/user/" + user # Replace with the URL of the page you want to scrape
|
||||
|
||||
# Fetch the web page content
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
html_content = response.text
|
||||
else:
|
||||
return f"Error: Unable to fetch the page. Status code: {response.status_code}"
|
||||
|
||||
# Parse the HTML content using BeautifulSoup
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
|
||||
""" title = soup.title.text """
|
||||
# Find the 'img' tag inside the 'a' tag with class 'current' inside the 'userpage-nav-avatar' tag
|
||||
img_tag = soup.select_one('userpage-nav-avatar a.current img')
|
||||
|
||||
if img_tag:
|
||||
# Extract the 'src' attribute of the 'img' tag to get the image URL
|
||||
image_url = img_tag['src']
|
||||
return image_url
|
||||
else:
|
||||
stderr.write("Image not found on the page.")
|
||||
|
||||
#return title
|
||||
|
||||
def compute_file_hash(self, file):
|
||||
try:
|
||||
# Compute BLAKE3 hash of the file
|
||||
hasher = blake3()
|
||||
with open(file, 'rb') as f:
|
||||
while chunk := f.read(65536):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f"Error computing file hash: {e}"))
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def save_image_to_model(image_url):
|
||||
try:
|
||||
# Download the image from the URL
|
||||
response = requests.get(image_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Create a new instance of YourModel
|
||||
instance = F()
|
||||
|
||||
# Save the image to the FileField
|
||||
instance.image_field.save(f'image_{instance.pk}.jpg', ContentFile(response.content), save=True)
|
||||
|
||||
# Save the model instance to the database
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Failed to download the image: {e}")
|
||||
|
||||
return None
|
||||
|
||||
@shared_task
|
||||
def test_task():
|
||||
print("This is a test task. Celery is working!")
|
||||
num = 12 * 2
|
||||
return num
|
||||
|
||||
@shared_task
|
||||
def calculate_square(number1, number2):
|
||||
result = number1 * number2
|
||||
return result
|
|
@ -1,63 +0,0 @@
|
|||
# custom_filters.py
|
||||
from django import template
|
||||
|
||||
import nh3
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
def is_html(string):
|
||||
'''
|
||||
Check if string is HTML
|
||||
'''
|
||||
soup = BeautifulSoup(string, "html.parser")
|
||||
# Remove leading and trailing white space
|
||||
stripped_string = string.strip()
|
||||
stripped_soup = str(soup).strip()
|
||||
# If the string remained the same after parsing with BeautifulSoup, it's probably not HTML
|
||||
if stripped_string == stripped_soup:
|
||||
return False
|
||||
# If the string changed when parsed by BeautifulSoup, it's probably HTML
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_image(file_url):
|
||||
image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp']
|
||||
return file_url.lower().endswith(tuple(image_extensions))
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_flash(file_url):
|
||||
flash_extensions = ['.swf']
|
||||
return file_url.lower().endswith(tuple(flash_extensions))
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_pdf(file_url):
|
||||
pdf_extensions = ['.pdf']
|
||||
return file_url.lower().endswith(tuple(pdf_extensions))
|
||||
|
||||
@register.filter
|
||||
def descriptionHtml2Text(description):
|
||||
if is_html(description):
|
||||
cleanHtml = nh3.clean(description)
|
||||
soup = BeautifulSoup(cleanHtml, "html.parser")
|
||||
text = soup.get_text()
|
||||
return text
|
||||
else:
|
||||
return description
|
||||
|
||||
|
||||
@register.filter
|
||||
def stylizeDescription(description):
|
||||
if description.startswith("<div"):
|
||||
stylizedDescription = nh3.clean(description)
|
||||
|
||||
else:
|
||||
stylizedDescription = description
|
||||
|
||||
return stylizedDescription
|
|
@ -1,26 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
|
||||
from .views import home, submission_detail, fa_user_page, fa_users, fa_tags, tag_details, fa_config, fa_import, simple_upload, search_results, stream_task_output, stream_datetime, stats
|
||||
|
||||
|
||||
app_name = "fa"
|
||||
urlpatterns = [
|
||||
path('fa/', home, name="home"),
|
||||
path('fa/sup', simple_upload, name="simple_upload"),
|
||||
path('fa/config', fa_config, name="config"),
|
||||
path("fa/import", fa_import, name="import"),
|
||||
path("fa/stats", stats, name="stats"),
|
||||
path("fa/stream_datetime", stream_datetime, name=""),
|
||||
path('fa/stream_task_output/<str:task_id>/', stream_task_output, name='stream_task_output'),
|
||||
path('fa/search_results', search_results, name='search_results'),
|
||||
path('fa/view/<int:submission_id>', submission_detail, name="submission_detail"),
|
||||
path('fa/users', fa_users, name="user_list"),
|
||||
path('fa/user/<str:fa_user>', fa_user_page, name="user_page"),
|
||||
path('fa/tags', fa_tags, name="tags"),
|
||||
path('fa/tag/<str:tag>', tag_details, name="tag_details"),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
@ -1,284 +0,0 @@
|
|||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseRedirect, StreamingHttpResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q, Case, When, Value, IntegerField
|
||||
|
||||
from .forms import SearchForm, URLImportForm
|
||||
|
||||
from .models import FA_User, FA_Tags, FA_Submission, FA_Submission_File
|
||||
|
||||
from .tasks import scrape_fa_submission, fa_import_data, test_task, calculate_square
|
||||
|
||||
import time, datetime, math
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import permissions
|
||||
from .serializers import UserSerializer, GroupSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows users to be viewed or edited.
|
||||
"""
|
||||
queryset = User.objects.all().order_by('-date_joined')
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows groups to be viewed or edited.
|
||||
"""
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
# Create your views here.
|
||||
def handle_task_result(task_result):
|
||||
# Handle the task result here
|
||||
print("Task completed! Result:", task_result)
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def home(request):
|
||||
new_archives = FA_Submission.objects.order_by('-date_added')[:10]
|
||||
new_submissions = FA_Submission.objects.order_by('-date')[:10]
|
||||
context = {'new_archives': new_archives, "new_submissions": new_submissions}
|
||||
return render(request, 'fa/home.html', context)
|
||||
|
||||
|
||||
# View Furaffinity submissions
|
||||
@login_required(login_url="/login/")
|
||||
def submission_detail(request, submission_id):
|
||||
submission = get_object_or_404(FA_Submission, submission_id=submission_id)
|
||||
file_url = submission.file.file.url if submission.file else None
|
||||
return render(request, 'fa/submission_detail.html', {'submission': submission, 'file_url': file_url})
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def search_results(request):
|
||||
'''
|
||||
|
||||
'''
|
||||
|
||||
# Define a function to calculate relevance
|
||||
def calculate_relevance(queryset, search_query, fields):
|
||||
'''
|
||||
Calculate relevance scores for items in a queryset based on a search query.
|
||||
|
||||
Parameters:
|
||||
queryset (QuerySet): The initial queryset that you want to annotate with relevance scores.
|
||||
search_query (str): The query string you want to search for within the specified fields.
|
||||
fields (list): A list of fields within the queryset where you want to search for the search_query.
|
||||
|
||||
Returns:
|
||||
QuerySet: A queryset annotated with a "relevance" score for each item.
|
||||
'''
|
||||
# Create a list of "When" conditions for each field
|
||||
conditions = [When(**{f"{field}__icontains": search_query, 'then': Value(1)}) for field in fields]
|
||||
# Annotate the queryset with a "relevance" score
|
||||
# The "Case" expression evaluates the conditions and assigns a score of 1 if the search_query is found in any of the specified fields,
|
||||
# otherwise, it assigns a score of 0.
|
||||
return queryset.annotate(relevance=Case(*conditions, default=Value(0), output_field=IntegerField()))
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
form = SearchForm(request.GET)
|
||||
if form.is_valid():
|
||||
search_query = form.cleaned_data['search_query']
|
||||
## Perform the search on your data
|
||||
#title_description_results = FA_Submission.objects.filter(Q(title__icontains=search_query) | Q(description__icontains=search_query))
|
||||
#tag_results = FA_Submission.objects.filter(tags__tag__icontains=search_query)
|
||||
#
|
||||
## Merge the title/content results and tag results, and remove duplicates
|
||||
#search_results = (title_description_results | tag_results).distinct()
|
||||
|
||||
|
||||
# Check if search_query starts with '#'
|
||||
if search_query.startswith('#'):
|
||||
# Strip '#' and split by spaces to get a list of tags
|
||||
tagshas = search_query.split()
|
||||
tags = []
|
||||
for tag in tagshas:
|
||||
tags.append(tag.lstrip('#'))
|
||||
print(tags)
|
||||
# Create Q objects for each tag and chain them using OR
|
||||
q_objects = Q()
|
||||
for tag in tags:
|
||||
q_objects |= Q(tags__tag__icontains=tag)
|
||||
|
||||
print(q_objects)
|
||||
# Filter using the Q objects
|
||||
tag_results = calculate_relevance(FA_Submission.objects.filter(tags__tag__icontains=q_objects), q_objects, ['tags__tag'])
|
||||
print(tag_results)
|
||||
|
||||
#search_results = calculate_relevance(FA_Submission.objects.filter(tags__tag__icontains=search_query), search_query, ['tags__tag'])
|
||||
# Order by relevance and remove duplicates
|
||||
search_results = tag_results.order_by('-relevance').distinct()
|
||||
#search_results = FA_Submission.objects.filter(tags__tag__icontains=search_query)
|
||||
else:
|
||||
# Perform the search on data
|
||||
# Calculate relevance for each queryset
|
||||
title_description_results = calculate_relevance(FA_Submission.objects.filter(Q(title__icontains=search_query) | Q(description__icontains=search_query)), search_query, ['title', 'description'])
|
||||
tag_results = calculate_relevance(FA_Submission.objects.filter(tags__tag__icontains=search_query), search_query, ['tags__tag'])
|
||||
|
||||
# Merge the results, order by relevance and remove duplicates
|
||||
search_results = (title_description_results | tag_results).order_by('-relevance').distinct()
|
||||
|
||||
|
||||
# Set the number of submissions to display per page
|
||||
per_page = 10
|
||||
|
||||
paginator = Paginator(search_results, per_page)
|
||||
page_number = request.GET.get('page') # Get the current page number from the URL parameter
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Render the search results to a template
|
||||
context = {'search_results': page_obj}
|
||||
return render(request, 'fa/search_results.html', context)
|
||||
else:
|
||||
form = SearchForm()
|
||||
return render(request, 'fa/search_results.html', {'form': form})
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def fa_users(request):
|
||||
list_users = FA_User.objects.order_by('artist')
|
||||
context = {'list_users': list_users}
|
||||
return render(request, 'fa/users.html', context)
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def fa_user_page(request, fa_user):
|
||||
user = get_object_or_404(FA_User, artist_url=fa_user)
|
||||
user_submissions = FA_Submission.objects.filter(artist=user).order_by('-date')
|
||||
|
||||
# Set the number of submissions to display per page
|
||||
per_page = 10
|
||||
|
||||
paginator = Paginator(user_submissions, per_page)
|
||||
page_number = request.GET.get('page') # Get the current page number from the URL parameter
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {'user': user, 'page_obj': page_obj}
|
||||
return render(request, 'fa/user_page.html', context)
|
||||
#return HttpResponse("Your at FA_User index of %s." % fa_user)
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def fa_tags(request):
|
||||
list_tags = FA_Tags.objects.order_by("tag")
|
||||
context = {'list_tags': list_tags}
|
||||
return render(request, 'fa/tags.html', context)
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def tag_details(request, tag):
|
||||
tag_slug = get_object_or_404(FA_Tags, tag_slug=tag)
|
||||
submissions = FA_Submission.objects.filter(tags=tag_slug).order_by('-date')
|
||||
context = {'submissions': submissions, 'tag': tag_slug }
|
||||
return render(request, 'fa/tag_details.html', context)
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def fa_config(request):
|
||||
return render(request, 'fa_config.html')
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def fa_import(request):
|
||||
if request.method == 'POST':
|
||||
form = URLImportForm(request.POST)
|
||||
if form.is_valid():
|
||||
#print(form.cleaned_data['url'])
|
||||
result = scrape_fa_submission.delay(form.cleaned_data['url'])
|
||||
print(result)
|
||||
# Handle successful form submission
|
||||
# You can redirect to a success page or do other actions
|
||||
#return render(request, 'fa/import.html', {'form': form})
|
||||
return HttpResponseRedirect("/fa/")
|
||||
|
||||
else:
|
||||
form = URLImportForm()
|
||||
|
||||
return render(request, 'fa/import.html', {'form': form})
|
||||
|
||||
|
||||
def stream_task_output(request, task_id):
|
||||
def event_stream():
|
||||
task = run_import_data.AsyncResult(task_id)
|
||||
while not task.ready():
|
||||
task_output = get_task_output(task_id) # Implement a function to retrieve task output
|
||||
yield f"data: {task_output}\n\n"
|
||||
time.sleep(1) # Adjust the delay between updates
|
||||
|
||||
response = HttpResponse(event_stream(), content_type='text/event-stream')
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
def stream_datetime(request):
|
||||
def event_stream():
|
||||
while True:
|
||||
time.sleep(3)
|
||||
yield 'data: The server time is: %s\n\n' % datetime.datetime.now()
|
||||
|
||||
response = StreamingHttpResponse(event_stream(), content_type='text/event-stream')
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def stats(request):
|
||||
|
||||
def convert_size(size_bytes):
|
||||
if size_bytes == 0:
|
||||
return "0B"
|
||||
size_name = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
|
||||
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
s = round(size_bytes / p, 2)
|
||||
return "%s %s" % (s, size_name[i])
|
||||
|
||||
totalMediaSizeBytes = 0
|
||||
for files in FA_Submission_File.objects.all():
|
||||
totalMediaSizeBytes += files.file.size
|
||||
|
||||
|
||||
submissions = FA_Submission.objects.count()
|
||||
users = FA_User.objects.count()
|
||||
tags = FA_Tags.objects.count()
|
||||
mediaFiles = FA_Submission_File.objects.count()
|
||||
missingFiles = len(FA_Submission.objects.filter(file__isnull=True))
|
||||
totalMediaSize = convert_size(totalMediaSizeBytes)
|
||||
|
||||
context = {
|
||||
"submissions": submissions,
|
||||
"users": users,
|
||||
"tags": tags,
|
||||
"mediaFiles": mediaFiles,
|
||||
"missingFiles": missingFiles,
|
||||
"totalMediaSize": totalMediaSize,
|
||||
}
|
||||
return render(request, "fa/stats.html", context)
|
||||
|
||||
|
||||
@login_required(login_url="/login/")
|
||||
def simple_upload(request):
|
||||
if request.method == 'POST' and request.FILES['myfile']:
|
||||
myfile = request.FILES['myfile']
|
||||
fs = FileSystemStorage()
|
||||
filename = fs.save(myfile.name, myfile)
|
||||
uploaded_file_url = fs.url(filename)
|
||||
return render(request, 'fa/simple_upload.html', {
|
||||
'uploaded_file_url': uploaded_file_url
|
||||
})
|
||||
return render(request, 'fa/simple_upload.html')
|
||||
|
||||
#class FA_ImportView(generic.DetailView):
|
||||
# pass
|
|
@ -1,52 +0,0 @@
|
|||
<!-- base.html -->
|
||||
|
||||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Gallery Archivist{% endblock %}</title>
|
||||
<link rel="icon" type="image/x-icon" href="{% static 'img/favicon.png' %}">
|
||||
{% block styles %}
|
||||
{% endblock styles %}
|
||||
<link rel="stylesheet" href="{% static 'libs/bootstrap/bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% include 'navbar.html' %}
|
||||
|
||||
<div class="container-fluid">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="externalLinkConfirmationModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" aria-labelledby="externalLinkConfirmationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h5 class="modal-title" id="externalLinkConfirmationModalLabel">Confirmation</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You are leaving this site and visiting an external link. Do you want to proceed?</p>
|
||||
<p id="externalLinkShow"></p>
|
||||
</div>
|
||||
<div class="modal-footer bg-warning">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a id="externalLink" href="#" target="_blank" rel="noopener noreferrer" class="btn btn-primary">Proceed</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
<script src="{% static 'js/confirm_external_links.js' %}"></script>
|
||||
<script src="{% static 'libs/bootstrap/bootstrap.bundle.min.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,62 +0,0 @@
|
|||
<!-- home.html -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Home | Furaffinity Archive{% endblock %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-2">
|
||||
<h1>New Archive's</h1>
|
||||
<div class="gallery-container">
|
||||
{% for submission in new_archives %}
|
||||
<a class="img-holder img-holder-small" href="{% url 'fa:submission_detail' submission_id=submission.submission_id %}">
|
||||
<div class="layer">
|
||||
<h3>{{ submission.title }}</h3>
|
||||
<p>{{ submission.date |date:'Y-m-d H:i' }}</p>
|
||||
</div>
|
||||
{% if submission.file %}
|
||||
{% if submission.file.file.url|is_image %}
|
||||
{% if submission.mature_rating.mature == "Adult" or submission.mature_rating.mature == "Mature" %}
|
||||
<img class="blur" src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% else %}
|
||||
<img src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<h1>New Submission's</h1>
|
||||
<div class="gallery-container">
|
||||
{% for submission in new_submissions %}
|
||||
<a class="img-holder img-holder-small" href="{% url 'fa:submission_detail' submission_id=submission.submission_id %}">
|
||||
<div class="layer">
|
||||
<h3>{{ submission.title }}</h3>
|
||||
<p>{{ submission.date |date:'Y-m-d H:i' }}</p>
|
||||
</div>
|
||||
{% if submission.file %}
|
||||
{% if submission.file.file.url|is_image %}
|
||||
{% if submission.mature_rating.mature == "Adult" or submission.mature_rating.mature == "Mature" %}
|
||||
<img class="blur" src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% else %}
|
||||
<img src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,18 +0,0 @@
|
|||
<!-- home.html -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Import | Furaffinity Archive{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h2>Your att the import screen.</h2>
|
||||
<form method="post" class="mb-3">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,100 +0,0 @@
|
|||
<!-- search_results.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Submission Search Results{% endblock %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<h1> Search Results</h1>
|
||||
<ul>
|
||||
{% for submission in search_results %}
|
||||
<li>
|
||||
<a class="row" href="{% url 'fa:submission_detail' submission_id=submission.submission_id %}">
|
||||
<div class="col-auto img-holder-small">
|
||||
{% if submission.file %}
|
||||
{% if submission.file.file.url|is_image %}
|
||||
{% if submission.mature_rating.mature == "Adult" or submission.mature_rating.mature == "Mature" %}
|
||||
<img class="blur" src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% else %}
|
||||
<img src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ submission.title }}</h3>
|
||||
<strong>By Artist: {{ submission.artist.artist }}</strong>
|
||||
<p>{{ submission.description|descriptionHtml2Text|truncatechars:200 }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>No submissions found.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row justify-content-center border border-2">
|
||||
|
||||
<!-- Display Bootstrap pagination -->
|
||||
<div class="col-6">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if search_results.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?search_query={{ request.GET.search_query }}&page=1" aria-label="First">
|
||||
«
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?search_query={{ request.GET.search_query }}&page={{ search_results.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">‹</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">
|
||||
Page {{ search_results.number }} of {{ search_results.paginator.num_pages }}.
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% if search_results.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?search_query={{ request.GET.search_query }}&page={{ search_results.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">›</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?search_query={{ request.GET.search_query }}&page={{ search_results.paginator.num_pages }}" aria-label="Last">
|
||||
»
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Jump-to field -->
|
||||
<div class="col-3">
|
||||
<form method="GET" class="row g-3">
|
||||
<input type="hidden" name="search_query" class="form-control" value="{{ request.GET.search_query }}">
|
||||
<label for="jumpToPage" class="col-auto col-form-label">Jump to:</label>
|
||||
<div class="col-3">
|
||||
<input type="number" name="page" id="jumpToPage" class="form-control" min="1" max="{{ search_results.paginator.num_pages }}" value="{{ search_results.number }}" >
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary">Go</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,46 +0,0 @@
|
|||
<!-- stats_template.html -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Statistics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Furaffinity Archive Statistics</h1>
|
||||
<p>Some metrics from the Furaffinity Archive</p>
|
||||
<table class="table table-striped border border-2 rounded">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric:</th>
|
||||
<th>Count:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Submissions:</td>
|
||||
<td>{{ submissions }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Users:</td>
|
||||
<td>{{ users }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tags:</td>
|
||||
<td>{{ tags }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Media Files:</td>
|
||||
<td>{{ mediaFiles }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Submissions with no Media Files:</td>
|
||||
<td>{{ missingFiles }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Media Size:</td>
|
||||
<td>{{ totalMediaSize }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Add other statistics here -->
|
||||
{% endblock content %}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
<!-- submission_detail.html -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{submission.title}} by {{submission.artist.artist}} | Submission Detail | Gallery-Archivist{% endblock %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-xl-9">
|
||||
<h1 class="mt-2">{{ submission.title }}</h1>
|
||||
<div id="submission_container">
|
||||
{% if file_url %}
|
||||
{% if file_url|is_image %}
|
||||
<img src="{{ file_url }}" alt="Image" width="100%" height="auto">
|
||||
{% elif submission.file.file.url|is_flash %}
|
||||
<div id="flash_embed"></div>
|
||||
{% elif submission.file.file.url|is_pdf %}
|
||||
<object data="{{ submission.file.file.url }}" type="application/pdf" width="100%" height="600px">
|
||||
<iframe src="{{ submission.file.file.url }}" width="100%" height="100%">
|
||||
This browser does not support PDFs. Please download the PDF to view it:
|
||||
<a href="{{ submission.file.file.url }}">Download PDF</a>
|
||||
</iframe>
|
||||
</object>
|
||||
{% else %}
|
||||
<div class="my-4 justify-content-center text-center">
|
||||
<p>No preview avilable of file.</p>
|
||||
<a class="btn btn-outline-primary" href="{{ file_url }}">Download File</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>No file attached.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="my-2 px-2 border border-2 overflow-y-scroll">
|
||||
{{ submission.description|stylizeDescription|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 bg-secondary-subtle border border-2">
|
||||
<div class="mt-2">
|
||||
<div class="row-cols-auto">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'fa:user_page' fa_user=submission.artist.artist_url %}">User Page</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ file_url }}">Download File</a>
|
||||
</div>
|
||||
<div class="m-2 overflow-hidden">
|
||||
<strong>Tags: </strong>
|
||||
{% if submission.tags.exists %}
|
||||
{% for tag in submission.tags.all %}
|
||||
<span class="">
|
||||
<a class="badge bg-primary tag" href="{% url 'fa:tag_details' tag.tag_slug %}">{{ tag.tag|capfirst }}</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">This submission has no tags</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="">
|
||||
<div>
|
||||
<strong class="text-info">Category:</strong>
|
||||
<span class="">{{ submission.fa_category }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="text-info">Species:</strong>
|
||||
<span class="">{{ submission.species }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="text-info">Gender:</strong>
|
||||
<span class="">{{ submission.gender }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="text-info">Size:</strong>
|
||||
<span class="">{{ submission.image_width }} x {{ submission.image_height }}</span>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div>
|
||||
<strong class="text-info">Mature Rating:</strong>
|
||||
<span class="badge bg-secondary text-2xl">{{ submission.mature_rating.mature }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="text-info">Orginal Date:</strong>
|
||||
<span class="">{{ submission.date |date:'Y-m-d H:i:s' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong class="text-info">Archive date:</strong>
|
||||
<span class="">{{ submission.date_added |date:'Y-m-d H:i:s' }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<a class="btn btn-outline-danger" href="https://www.furaffinity.net/view/{{submission.submission_id}}" target="_blank" rel="noopener noreferrer">View on Furaffinity...</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
|
||||
var flash_embed = document.getElementById('flash_embed');
|
||||
|
||||
if (flash_embed) {
|
||||
window.RufflePlayer = window.RufflePlayer || {};
|
||||
window.RufflePlayer.config = {
|
||||
"wmode": "direct",
|
||||
"quality": "high",
|
||||
}
|
||||
window.addEventListener("load", (event) => {
|
||||
const ruffle = window.RufflePlayer.newest();
|
||||
const player = ruffle.createPlayer();
|
||||
const container = document.getElementById("flash_embed");
|
||||
container.appendChild(player);
|
||||
//player.style.width = "auto";
|
||||
player.style.width = "{{ submission.image_width }}px";
|
||||
player.style.height = "{{ submission.image_height }}px";
|
||||
player.load("{{ submission.file.file.url }}").then(() => {
|
||||
console.info("Ruffle successfully loaded the file");
|
||||
}).catch((e) => {
|
||||
console.error(`Ruffle failed to load the file: ${e}`);
|
||||
});
|
||||
/* player.addEventListener('loadedmetadata', () => {
|
||||
console.info(player.metadata.isActionScript3);
|
||||
console.info(player.metadata.isPlaying);
|
||||
}); */
|
||||
});
|
||||
|
||||
|
||||
function resizeFlashEmbed() {
|
||||
var submission_container = document.getElementById("submission_container");
|
||||
var flash_embed = document.getElementById('flash_embed');
|
||||
var flash_embed_player = flash_embed.firstChild
|
||||
|
||||
var flash_width = parseInt("{{ submission.image_width }}");
|
||||
var flash_height = parseInt("{{ submission.image_height }}");
|
||||
|
||||
var container_width = submission_container.offsetWidth;
|
||||
var container_height = submission_container.offsetHeight;
|
||||
|
||||
if (flash_width < flash_height) {
|
||||
var originalAspectRatio = flash_height / flash_width;
|
||||
} else {
|
||||
var originalAspectRatio = flash_width / flash_height;
|
||||
};
|
||||
|
||||
flash_embed_player.style.width = container_width + 'px';
|
||||
flash_embed_player.style.height = (container_width * originalAspectRatio) + 'px';
|
||||
}
|
||||
|
||||
// Call the function on page load and window resize
|
||||
window.addEventListener('load', resizeFlashEmbed);
|
||||
window.addEventListener('resize', resizeFlashEmbed);
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<script src="{% static 'libs/ruffle-nightly-2023_05_04-web-selfhosted/ruffle.js' %}"></script>
|
||||
{% endblock %}
|
|
@ -1,35 +0,0 @@
|
|||
<!-- tag_details.html -->
|
||||
|
||||
<!-- tags.html -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ tag.tag}} | Tag Details | Gallery-Archivist{% endblock %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<a href="{% url 'fa:tags' %}">Back to Tags Page</a>
|
||||
<h1>{{ tag.tag }}</h1>
|
||||
<h2>Submissions</h2>
|
||||
{% if submissions %}
|
||||
<ul>
|
||||
{% for submission in submissions %}
|
||||
<li>
|
||||
<a href="{% url 'fa:submission_detail' submission_id=submission.submission_id %}">{{ submission.title }} by {{ submission.artist.artist }}</a>
|
||||
<p>{{ submission.description|truncatechars:100 }}</p>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No submissions yet.</p>
|
||||
{% endif %}
|
||||
<a href="https://www.furaffinity.net/view/">View on Furaffinity..</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,50 +0,0 @@
|
|||
<!-- tags.html -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %} Tags {% endblock %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<strong>Furaffinity's Tag list:</strong>
|
||||
<div class="m-2 overflow-hidden">
|
||||
{% if list_tags %}
|
||||
{% for tag in list_tags %}
|
||||
<span class="">
|
||||
<a class="badge bg-primary tag" href="{% url 'fa:tag_details' tag.tag_slug %}">{{ tag.tag|capfirst }}</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">This submission has no tags</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% comment %} <ul>
|
||||
{% for tag in list_tags %}
|
||||
<li><a href="{% url 'fa:tag_details' tag.tag_slug %}">{{ tag.tag }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div>
|
||||
<strong>Tags:</strong>
|
||||
{% for tag in list_tags %}
|
||||
<span class="badge bg-primary">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No tags available.</p> {% endcomment %}
|
||||
<div>
|
||||
<p>Links:</p>
|
||||
<ul>
|
||||
<li><a href="users">Users</a></li>
|
||||
<li><a href="config">Config</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,129 +0,0 @@
|
|||
<!-- user_page.html -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ user.artist }}'s User Page{% endblock %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row g-4 mt-4">
|
||||
<h1 class="">Archive of {{ user.artist }}'s Submissions</h1>
|
||||
<div class="gallery-container">
|
||||
{% for submission in page_obj %}
|
||||
<a class="img-holder" href="{% url 'fa:submission_detail' submission_id=submission.submission_id %}">
|
||||
<div class="layer">
|
||||
<h3>{{ submission.title }}</h3>
|
||||
<p>{{ submission.date |date:'Y-m-d H:i' }}</p>
|
||||
<p class="badge bg-secondary text-2xl"> {{ submission.mature_rating.mature }}</p>
|
||||
</div>
|
||||
{% if submission.file %}
|
||||
{% if submission.file.file.url|is_image %}
|
||||
{% if submission.mature_rating.mature == "Adult" or submission.mature_rating.mature == "Mature" %}
|
||||
<img class="blur" src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% else %}
|
||||
<img src="{{ submission.file.file.url }}" alt="{{ submission.title }}">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{% static 'img/no-image-dark.webp' %}" alt="No image">
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center border border-2">
|
||||
|
||||
<!-- Display Bootstrap pagination -->
|
||||
<div class="col-6">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1" aria-label="First">
|
||||
«
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">‹</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">›</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
|
||||
»
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Jump-to field -->
|
||||
<div class="col-3">
|
||||
<form method="GET" class="row g-3">
|
||||
<label for="jumpToPage" class="col-auto col-form-label">Jump to:</label>
|
||||
<div class="col-3">
|
||||
<input type="number" name="page" id="jumpToPage" class="form-control" min="1" max="{{ page_obj.paginator.num_pages }}" value="{{ page_obj.number }}">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary">Go</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% comment %} <div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1">« First</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">Next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}">Last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div> {% endcomment %}
|
||||
{% endblock %}
|
||||
|
||||
{% comment %} {% block scripts %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
// Enable Bootstrap tooltips
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %} {% endcomment %}
|
|
@ -1,19 +0,0 @@
|
|||
<div>
|
||||
<h2>Archived Furaffinity user list:</h2>
|
||||
{% if list_users %}
|
||||
<ul>
|
||||
{% for user in list_users %}
|
||||
<li><a href="{% url 'fa:user_page' user.artist_url %}"> {{ user.artist }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No users available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<p>Links:</p>
|
||||
<ul>
|
||||
<li><a href="tags">Tags</a></li>
|
||||
<li><a href="config">Config</a></li>
|
||||
</ul>
|
||||
</div>
|
|
@ -1,12 +0,0 @@
|
|||
<!-- footer.html -->
|
||||
|
||||
{% comment %} {% load render_time %} {% endcomment %}
|
||||
|
||||
{% comment %} {% block footer %} {% endcomment %}
|
||||
<div class="bg-body-secondary border border-top-2 border-primary-subtle ">
|
||||
<div class="col mt-3 justify-content-center text-center">
|
||||
<span class="badge bg-danger">Note still in heavy development</span>
|
||||
<p class="">Gallery-Archivist is made by <a href="https://aroy-art.com">Aroy</a> and you can find the <a href="https://git.aroy-art.com/Aroy/Gallery-Archivist">source here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% comment %} {% endblock %} {% endcomment %}
|
|
@ -1,109 +0,0 @@
|
|||
<!-- navbar.html -->
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% comment %} <nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Navbar</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="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
Action
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
Another action
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
Something else here
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled">
|
||||
Disabled
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav> {% endcomment %}
|
||||
|
||||
<nav class="navbar navbar-expand-lg sticky-top bg-secondary-subtle border-bottom border-2 border-primary-subtle">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{% url "fa:home" %}">
|
||||
<img src="{% static 'img/favicon.png' %}" height="24">
|
||||
Furaffinity Archive
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarMain">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'fa:user_list' %}">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'fa:tags' %}">Tags</a>
|
||||
</li>
|
||||
|
||||
<!-- Empty space in the middle -->
|
||||
<div class="navbar-text"> </div>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
More
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'fa:import' %}">Import</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'fa:stats' %}">Stats</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/admin">Djnago Admin</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex justify-content-end" role="search" method="GET" action="{% url 'fa:search_results' %}">
|
||||
{% comment %} {% csrf_token %} {% endcomment %}
|
||||
<input class="form-control me-2 bg-light" type="search" name="search_query" placeholder="Search" aria-label="Search">
|
||||
<button class="btn btn-outline-primary" type="submit">Search</button>
|
||||
</form>
|
||||
<ul>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,708 +0,0 @@
|
|||
/* Main CSS */
|
||||
|
||||
/* Override Bootstrap variables for the dark theme */
|
||||
/* :root {
|
||||
--bs-body-color: #fff;
|
||||
--bs-body-bg: #030918;
|
||||
--bs-secondary-bg: #0c1827; */
|
||||
|
||||
/* --bs-dropdown-link-hover-bg: #345;
|
||||
--bs-dropdown-link-color: #000;
|
||||
--bs-navbar-active-color: #000; */
|
||||
/* } */
|
||||
|
||||
/* :root, [data-bs-theme="light"] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13,110,253;
|
||||
--bs-secondary-rgb: 108,117,125;
|
||||
--bs-success-rgb: 25,135,84;
|
||||
--bs-info-rgb: 13,202,240;
|
||||
--bs-warning-rgb: 255,193,7;
|
||||
--bs-danger-rgb: 220,53,69;
|
||||
--bs-light-rgb: 248,249,250;
|
||||
--bs-dark-rgb: 33,37,41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255,255,255;
|
||||
--bs-black-rgb: 0,0,0;
|
||||
--bs-font-sans-serif: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33,37,41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255,255,255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0,0,0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33,37,41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233,236,239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33,37,41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248,249,250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13,110,253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10,88,202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
} */
|
||||
|
||||
/*=================================================================
|
||||
Electric container style
|
||||
=================================================================*/
|
||||
@keyframes gradient {
|
||||
0% {background-position: 0% 50%;}
|
||||
50% {background-position: 0% 0%;}
|
||||
100% {background-position: 0% 50%;}
|
||||
}
|
||||
|
||||
@keyframes wiremove {
|
||||
0% {background-position: 0px 0px;}
|
||||
100% {background-position: 0px 1500px;}
|
||||
}
|
||||
|
||||
body{
|
||||
/*! color:#ffffff; */
|
||||
margin:0px;
|
||||
background: linear-gradient(0deg, #9e14d0, #e73c7e, #23a6d5, #23d5ab, #23a6d5, #e73c7e, #9e14d0);
|
||||
background-size: 100% 800%;
|
||||
background-attachment:fixed;
|
||||
animation: gradient 25s ease infinite;
|
||||
}
|
||||
|
||||
.everything{
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.wires{
|
||||
background-image: url( "/static/img/bg/wire-bg.png");
|
||||
background-attachment:fixed;
|
||||
padding: 7rem 0;
|
||||
animation: wiremove 44s linear infinite;
|
||||
/*! text-align:center; */
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
|
||||
/*=================================================================
|
||||
Electric container style
|
||||
=================================================================*/
|
||||
.e-container-border{
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color:#222222;
|
||||
background: linear-gradient(180deg, #4b8fca, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
box-shadow: 0px 2px 10px 0px #221133;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.3s;
|
||||
}
|
||||
|
||||
.e-container{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
background-color: #222222;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.e-container-radius{
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
|
||||
/*=================================================================
|
||||
Responsive image gallery old
|
||||
=================================================================*/
|
||||
.gallery-container {
|
||||
max-width: auto;
|
||||
margin: 32px auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.img-holder {
|
||||
flex-grow:1;
|
||||
margin: .2rem;
|
||||
overflow: hidden;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border: 2px #707070 solid;
|
||||
border-radius: 8px;
|
||||
background-color: #707070;
|
||||
}
|
||||
|
||||
.img-holder img {
|
||||
padding: 0px;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 50%;
|
||||
min-width: 100%;
|
||||
height: 24rem;
|
||||
object-fit: cover;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.img-holder img.blur {
|
||||
filter: blur(1.5rem);
|
||||
}
|
||||
|
||||
.img-holder-small img {
|
||||
height: 12rem !important;
|
||||
}
|
||||
|
||||
.img-holder-small img.blur {
|
||||
filter: blur(1.5rem);
|
||||
}
|
||||
|
||||
.img-holder .layer {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
display: grid;
|
||||
margin: 0 auto;
|
||||
width: 50%;
|
||||
min-width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: #0a030e;
|
||||
color: #fff;
|
||||
grid-row: 1;
|
||||
transition: all 0.9s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.img-holder .layer p {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.9s ease;
|
||||
/* transform: scale(0.8); */
|
||||
}
|
||||
|
||||
.img-holder .layer h3 {
|
||||
text-align: center;
|
||||
font-size: 100%;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.9s ease;
|
||||
/* transform: scale(0.8); */
|
||||
}
|
||||
|
||||
.img-holder:hover .layer {
|
||||
opacity: 0.8;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
}
|
||||
|
||||
/* .img-holder:hover .layer p {
|
||||
transform: scale(1);
|
||||
transition: all 0.9s ease;
|
||||
}
|
||||
|
||||
.img-holder:hover .layer h3 {
|
||||
transform: scale(1);
|
||||
transition: all 0.9s ease;
|
||||
} */
|
||||
|
||||
|
||||
.tag {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
/* transition: all 0; */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/*=================================================================
|
||||
Responsive image gallery
|
||||
=================================================================*/
|
||||
|
||||
.gallery-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 20px; /* Adjust the gap between items */
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
height: 20rem;
|
||||
position: relative; /* Positioning context for the overlay */
|
||||
overflow: hidden; /* Ensure that the content does not overflow the set height */
|
||||
display: flex; /* Use Flexbox for vertical centering */
|
||||
justify-content: center; /* Center horizontally */
|
||||
align-items: center; /* Center vertically */
|
||||
border-radius: 1vmin;
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
max-height: 100%;
|
||||
/* min-width: 50%; */
|
||||
object-fit: contain;
|
||||
vertical-align: bottom;
|
||||
border-radius: 1vmin;
|
||||
}
|
||||
|
||||
.gallery-item img.blur {
|
||||
filter: blur(1.5rem);
|
||||
}
|
||||
|
||||
/* ADVANCED */
|
||||
|
||||
/* Portrait */
|
||||
|
||||
@media (max-aspect-ratio: 1/1) {
|
||||
.gallery-item {
|
||||
height: 30rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Short screens */
|
||||
|
||||
@media (max-height: 480px) {
|
||||
.gallery-item {
|
||||
height: 80rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smaller screens in portrait */
|
||||
|
||||
@media (max-aspect-ratio: 1/1) and (max-width: 480px) {
|
||||
.gallery-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%;
|
||||
max-height: 75vh;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8); /* Semi-transparent black background */
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0; /* Hide the overlay by default */
|
||||
transition: opacity 0.3s ease; /* Smooth transition for the overlay */
|
||||
}
|
||||
|
||||
.gallery-item:hover .overlay {
|
||||
opacity: 1; /* Show the overlay on hover */
|
||||
}
|
||||
|
||||
.seen-overlay {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
letter-spacing: .5px;
|
||||
color: #232326;
|
||||
background-color: #414141;
|
||||
border: 1.25px solid #232326;
|
||||
border-radius: 0 1vmin 0 1vmin;
|
||||
white-space: nowrap;
|
||||
padding: 4px 6px 4px;
|
||||
margin: 0 1px 0 0;
|
||||
cursor: pointer;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.seen-overlay[data-seen="false"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*=================================================================
|
||||
Site icon overlay on gallery
|
||||
=================================================================*/
|
||||
|
||||
.site-btn-overlay {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 0;
|
||||
top: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
letter-spacing: .5px;
|
||||
width: 30px;
|
||||
color: #232326;
|
||||
background-color: #414141;
|
||||
border: 1.25px solid #232326;
|
||||
border-radius: 0 1vmin 0 1vmin;
|
||||
white-space: nowrap;
|
||||
padding: 4px 4px 5px;
|
||||
margin: 0 0 0 1px;
|
||||
cursor: pointer;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.site-btn-overlay img {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
|
||||
/*=================================================================
|
||||
Glitch text on 404 Page
|
||||
=================================================================*/
|
||||
|
||||
.glitch {
|
||||
position: relative;
|
||||
color: white;
|
||||
font-size: 8em;
|
||||
letter-spacing: 0.5em;
|
||||
animation: glitch-skew 1s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
.glitch::before {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 2px;
|
||||
text-shadow: -2px 0 #ff00c1;
|
||||
clip: rect(44px, 450px, 56px, 0);
|
||||
animation: glitch-anim 5s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
.glitch::after {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: -2px;
|
||||
text-shadow: -2px 0 #00fff9, 2px 2px #ff00c1;
|
||||
animation: glitch-anim2 1s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
@keyframes glitch-anim {
|
||||
0% {
|
||||
clip: rect(80px, 9999px, 31px, 0);
|
||||
transform: skew(1deg);
|
||||
}
|
||||
5% {
|
||||
clip: rect(87px, 9999px, 12px, 0);
|
||||
transform: skew(0.31deg);
|
||||
}
|
||||
10% {
|
||||
clip: rect(34px, 9999px, 13px, 0);
|
||||
transform: skew(0.56deg);
|
||||
}
|
||||
15% {
|
||||
clip: rect(82px, 9999px, 74px, 0);
|
||||
transform: skew(0.69deg);
|
||||
}
|
||||
20% {
|
||||
clip: rect(1px, 9999px, 72px, 0);
|
||||
transform: skew(0.52deg);
|
||||
}
|
||||
25% {
|
||||
clip: rect(35px, 9999px, 54px, 0);
|
||||
transform: skew(0.5deg);
|
||||
}
|
||||
30% {
|
||||
clip: rect(62px, 9999px, 2px, 0);
|
||||
transform: skew(0.17deg);
|
||||
}
|
||||
35% {
|
||||
clip: rect(6px, 9999px, 28px, 0);
|
||||
transform: skew(0.12deg);
|
||||
}
|
||||
40% {
|
||||
clip: rect(50px, 9999px, 85px, 0);
|
||||
transform: skew(0.88deg);
|
||||
}
|
||||
45% {
|
||||
clip: rect(34px, 9999px, 53px, 0);
|
||||
transform: skew(0.98deg);
|
||||
}
|
||||
50% {
|
||||
clip: rect(15px, 9999px, 58px, 0);
|
||||
transform: skew(0.15deg);
|
||||
}
|
||||
55% {
|
||||
clip: rect(20px, 9999px, 68px, 0);
|
||||
transform: skew(0.06deg);
|
||||
}
|
||||
60% {
|
||||
clip: rect(77px, 9999px, 52px, 0);
|
||||
transform: skew(0.02deg);
|
||||
}
|
||||
65% {
|
||||
clip: rect(20px, 9999px, 11px, 0);
|
||||
transform: skew(0.2deg);
|
||||
}
|
||||
70% {
|
||||
clip: rect(97px, 9999px, 57px, 0);
|
||||
transform: skew(0.17deg);
|
||||
}
|
||||
75% {
|
||||
clip: rect(1px, 9999px, 36px, 0);
|
||||
transform: skew(1deg);
|
||||
}
|
||||
80% {
|
||||
clip: rect(31px, 9999px, 63px, 0);
|
||||
transform: skew(0.49deg);
|
||||
}
|
||||
85% {
|
||||
clip: rect(80px, 9999px, 39px, 0);
|
||||
transform: skew(0.78deg);
|
||||
}
|
||||
90% {
|
||||
clip: rect(74px, 9999px, 60px, 0);
|
||||
transform: skew(0.96deg);
|
||||
}
|
||||
95% {
|
||||
clip: rect(97px, 9999px, 59px, 0);
|
||||
transform: skew(0.46deg);
|
||||
}
|
||||
100% {
|
||||
clip: rect(56px, 9999px, 84px, 0);
|
||||
transform: skew(0.12deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glitch-anim2 {
|
||||
0% {
|
||||
clip: rect(39px, 9999px, 61px, 0);
|
||||
transform: skew(0.08deg);
|
||||
}
|
||||
5% {
|
||||
clip: rect(13px, 9999px, 77px, 0);
|
||||
transform: skew(0.62deg);
|
||||
}
|
||||
10% {
|
||||
clip: rect(16px, 9999px, 14px, 0);
|
||||
transform: skew(0.35deg);
|
||||
}
|
||||
15% {
|
||||
clip: rect(83px, 9999px, 24px, 0);
|
||||
transform: skew(0.81deg);
|
||||
}
|
||||
20% {
|
||||
clip: rect(70px, 9999px, 14px, 0);
|
||||
transform: skew(0.93deg);
|
||||
}
|
||||
25% {
|
||||
clip: rect(43px, 9999px, 88px, 0);
|
||||
transform: skew(0.05deg);
|
||||
}
|
||||
30% {
|
||||
clip: rect(60px, 9999px, 63px, 0);
|
||||
transform: skew(0.89deg);
|
||||
}
|
||||
35% {
|
||||
clip: rect(35px, 9999px, 11px, 0);
|
||||
transform: skew(0.87deg);
|
||||
}
|
||||
40% {
|
||||
clip: rect(10px, 9999px, 4px, 0);
|
||||
transform: skew(0.63deg);
|
||||
}
|
||||
45% {
|
||||
clip: rect(56px, 9999px, 51px, 0);
|
||||
transform: skew(0.12deg);
|
||||
}
|
||||
50% {
|
||||
clip: rect(22px, 9999px, 21px, 0);
|
||||
transform: skew(0.26deg);
|
||||
}
|
||||
55% {
|
||||
clip: rect(45px, 9999px, 80px, 0);
|
||||
transform: skew(0.56deg);
|
||||
}
|
||||
60% {
|
||||
clip: rect(65px, 9999px, 18px, 0);
|
||||
transform: skew(0.08deg);
|
||||
}
|
||||
65% {
|
||||
clip: rect(71px, 9999px, 89px, 0);
|
||||
transform: skew(0.38deg);
|
||||
}
|
||||
70% {
|
||||
clip: rect(89px, 9999px, 98px, 0);
|
||||
transform: skew(0.71deg);
|
||||
}
|
||||
75% {
|
||||
clip: rect(63px, 9999px, 3px, 0);
|
||||
transform: skew(0.75deg);
|
||||
}
|
||||
80% {
|
||||
clip: rect(4px, 9999px, 68px, 0);
|
||||
transform: skew(0.98deg);
|
||||
}
|
||||
85% {
|
||||
clip: rect(22px, 9999px, 85px, 0);
|
||||
transform: skew(0.8deg);
|
||||
}
|
||||
90% {
|
||||
clip: rect(28px, 9999px, 58px, 0);
|
||||
transform: skew(0.06deg);
|
||||
}
|
||||
95% {
|
||||
clip: rect(70px, 9999px, 12px, 0);
|
||||
transform: skew(0.99deg);
|
||||
}
|
||||
100% {
|
||||
clip: rect(11px, 9999px, 87px, 0);
|
||||
transform: skew(0.15deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glitch-skew {
|
||||
0% {
|
||||
transform: skew(4deg);
|
||||
}
|
||||
10% {
|
||||
transform: skew(5deg);
|
||||
}
|
||||
20% {
|
||||
transform: skew(1deg);
|
||||
}
|
||||
30% {
|
||||
transform: skew(1deg);
|
||||
}
|
||||
40% {
|
||||
transform: skew(5deg);
|
||||
}
|
||||
50% {
|
||||
transform: skew(1deg);
|
||||
}
|
||||
60% {
|
||||
transform: skew(0deg);
|
||||
}
|
||||
70% {
|
||||
transform: skew(3deg);
|
||||
}
|
||||
80% {
|
||||
transform: skew(4deg);
|
||||
}
|
||||
90% {
|
||||
transform: skew(5deg);
|
||||
}
|
||||
100% {
|
||||
transform: skew(-1deg);
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 190 KiB |
Binary file not shown.
Before Width: | Height: | Size: 314 KiB |
Binary file not shown.
Before Width: | Height: | Size: 109 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.6 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue