First Upload
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM python:3.10.3-alpine
|
||||
|
||||
WORKDIR /rustdesk-api-server
|
||||
ADD . /rustdesk-api-server
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apk add --no-cache \
|
||||
gcc \
|
||||
musl-dev \
|
||||
mariadb-connector-c-dev \
|
||||
pkgconfig
|
||||
|
||||
RUN set -ex \
|
||||
&& pip install --no-cache-dir --disable-pip-version-check -r requirements.txt \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& cp -r ./db ./db_bak
|
||||
|
||||
ENV HOST="0.0.0.0"
|
||||
ENV TZ="Asia/Shanghai"
|
||||
|
||||
EXPOSE 21114/tcp
|
||||
EXPOSE 21114/udp
|
||||
|
||||
ENTRYPOINT ["sh", "run.sh"]
|
147
README.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# rustdesk-api-server
|
||||
|
||||
## based on https://github.com/kingmo888/rustdesk-api-server
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Supports self-registration and login on the front-end webpage.
|
||||
- Supports displaying device information on the front end, divided into administrator and user versions.
|
||||
- Supports custom aliases (remarks).
|
||||
- Supports backend management.
|
||||
- Supports colored tags.
|
||||
- Supports device online statistics.
|
||||
- Supports saving device passwords.
|
||||
- Automatically manages tokens and keeps them alive using the heartbeat interface.
|
||||
- Supports sharing devices with other users.
|
||||
- Supports web control terminal (currently only supports non-SSL mode, see below for usage issues)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# open to the directory you want to install the api server (change /opt to wherever you want)
|
||||
cd /opt
|
||||
# Clone the code locally
|
||||
git clone https://github.com/bryangerlach/rustdesk-api-server.git
|
||||
# Enter the directory
|
||||
cd rustdesk-api-server
|
||||
# setup a python virtual environment called rdgen
|
||||
python -m venv rustdesk-api
|
||||
# activate the python virtual environment
|
||||
source rustdesk-api/bin/activate
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
python manage.py migrate
|
||||
# After ensuring dependencies are installed correctly, execute:
|
||||
python manage.py runserver 0.0.0.0:21114
|
||||
```
|
||||
|
||||
## ## To autostart the server on boot, you can set up a systemd service called rustdeskapi.service
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Rustdesk API Server
|
||||
[Service]
|
||||
Type=simple
|
||||
LimitNOFILE=1000000
|
||||
ExecStart=/opt/rustdesk-api-server/rustdesk-api/bin/python3 /opt/rustdesk-api-server/manage.py runserver 0.0.0.0:21114
|
||||
WorkingDirectory=/opt/rustdesk-api-server/
|
||||
User=root
|
||||
Group=root
|
||||
Restart=always
|
||||
StandardOutput=file:/var/log/rustdesk/apiserver.log
|
||||
StandardError=file:/var/log/rustdesk/apiserver.error
|
||||
# Restart service after 10 seconds if node service crashes
|
||||
RestartSec=10
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
#this assumes you have cloned the repo into /opt/rustdesk/rustdesk-api-server and have a service named rustdeskapi set up
|
||||
systemctl stop rustdeskapi
|
||||
cd /opt/rustdesk/rustdesk-api-server
|
||||
git pull
|
||||
source rustdesk-api/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python manage.py migrate
|
||||
systemctl start rustdeskapi
|
||||
```
|
||||
|
||||
**Client Downlaods**: You will need to generate your own client downloads with your server and key hard coded into the program. The easiest way to do this is using github actions https://rustdesk.com/docs/en/dev/build/all/
|
||||
|
||||
Now you can access it using `http://localhostIP:Port`.
|
||||
|
||||
**Note**: When configuring on CentOS, Django4 may have problems due to the low version of sqlite3 in the system. Please modify the file in the dependency library. Path: `xxxx/Lib/site-packages/django/db/backends/sqlite3/base.py` (Find the package address according to the situation), modify the content:
|
||||
```python
|
||||
# from sqlite3 import dbapi2 as Database #(comment out this line)
|
||||
from pysqlite3 import dbapi2 as Database # enable pysqlite3
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable Name | Reference Value | Note |
|
||||
| ---- | ------- | ----------- |
|
||||
| `HOST` | Default `0.0.0.0` | IP binding of the service |
|
||||
| `TZ` | Default `Asia/Shanghai`, optional | Timezone |
|
||||
| `SECRET_KEY` | Optional, custom a random string | Program encryption key |
|
||||
| `CSRF_TRUSTED_ORIGINS` | Optional, verification off by default;<br>If you need to enable it, fill in your access address `http://yourdomain.com:21114` <br>**To disable verification, please delete this variable instead of leaving it blank** | Cross-origin trusted source |
|
||||
| `ID_SERVER` | Optional, default is the same host as the API server.<br>Customizable like `yourdomain.com` | ID server used by the web control terminal |
|
||||
| `DEBUG` | Optional, default `False` | Debug mode |
|
||||
| `ALLOW_REGISTRATION` | Optional, default `True` | Whether to allow new user registration |
|
||||
| Database Configuration | -- Start -- | If not using MYSQL, the following are unnecessary |
|
||||
| `DATABASE_TYPE` | Optional, default `SQLITE3` | Database type (SQLITE/MYSQL) |
|
||||
| `MYSQL_DBNAME` | Optional, default `-` | MYSQL database name |
|
||||
| `MYSQL_HOST` | Optional, default `127.0.0.1` | MYSQL database server IP |
|
||||
| `MYSQL_USER` | Optional, default `-` | MYSQL database username |
|
||||
| `MYSQL_PASSWORD` | Optional, default `-` | MYSQL database password |
|
||||
| `MYSQL_PORT` | Optional, default `3306` | MYSQL database port |
|
||||
| Database Configuration | -- End -- | See [sqlite3 migration to mysql tutorial](/tutorial/sqlite2mysql.md) |
|
||||
|
||||
## Usage Issues
|
||||
|
||||
- Administrator Settings
|
||||
|
||||
When there are no accounts in the database, the first registered account directly obtains super administrator privileges,
|
||||
|
||||
and subsequently registered accounts are ordinary accounts.
|
||||
|
||||
- Device Information
|
||||
|
||||
Tested, the client will send device information to the API interface regularly in the mode of installation as a service under non-green mode, so if you want device information, you need to install the Rustdesk client and start the service.
|
||||
|
||||
- Slow Connection Speed
|
||||
|
||||
Use the client generator to generate a client that has the connection delay removed
|
||||
|
||||
- Web Control Terminal Configuration
|
||||
|
||||
- Set the ID_SERVER environment variable or modify the ID_SERVER configuration item in the rustdesk_server_api/settings.py file and fill in the IP or domain name of the ID server/relay server.
|
||||
|
||||
- Web Control Terminal Keeps Spinning
|
||||
|
||||
- Check if the ID server filling is correct.
|
||||
|
||||
- The web control terminal currently only supports non-SSL mode. If the webui is accessed via https, remove the 's', otherwise ws cannot connect and keeps spinning. For example: https://domain.com/webui, change to http://domain.com/webui
|
||||
|
||||
- CSRF verification failed when logging in or logging out of backend operations. Request interrupted.
|
||||
|
||||
This operation is highly likely to be a combination of docker configuration + nginx reverse proxy + SSL. Pay attention to modifying CSRF_TRUSTED_ORIGINS. If it is SSL, it starts with https, otherwise it is http.
|
||||
|
||||
## Other Related Tools
|
||||
|
||||
- [rdgen](https://github.com/bryangerlach/rdgen)
|
||||
|
||||
- [infinite remote](https://github.com/infiniteremote/installer)
|
||||
|
||||
- [CMD script for modifying client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
|
||||
|
||||
- [rustdesk](https://github.com/rustdesk/rustdesk)
|
||||
|
||||
- [rustdesk-server](https://github.com/rustdesk/rustdesk-server)
|
210
README_EN.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# rustdesk-api-server
|
||||
|
||||
## If the project has helped you, giving a star isn't too much, right?
|
||||
|
||||
## Please use the latest version 1.2.3 of the client.
|
||||
|
||||
[点击这里查看中文说明。](https://github.com/kingmo888/rustdesk-api-server/blob/master/README.md)
|
||||
|
||||
<p align="center">
|
||||
<i>A Rustdesk API interface implemented in Python, with WebUI management support</i>
|
||||
<br/>
|
||||
<img src ="https://img.shields.io/badge/Version-1.5.0-blueviolet.svg"/>
|
||||
<img src ="https://img.shields.io/badge/Python-3.7|3.8|3.9|3.10|3.11-blue.svg" />
|
||||
<img src ="https://img.shields.io/badge/Django-3.2+|4.x-yelow.svg" />
|
||||
<br/>
|
||||
<img src ="https://img.shields.io/badge/Platform-Windows|Linux-green.svg"/>
|
||||
<img src ="https://img.shields.io/badge/Docker-arm|arm64|amd64-blue.svg" />
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Supports self-registration and login on the front-end webpage.
|
||||
- Registration and login pages:
|
||||

|
||||

|
||||
|
||||
- Supports displaying device information on the front end, divided into administrator and user versions.
|
||||
- Supports custom aliases (remarks).
|
||||
- Supports backend management.
|
||||
- Supports colored tags.
|
||||

|
||||
|
||||
- Supports device online statistics.
|
||||
- Supports saving device passwords.
|
||||
- Automatically manages tokens and keeps them alive using the heartbeat interface.
|
||||
- Supports sharing devices with other users.
|
||||

|
||||
- Supports web control terminal (currently only supports non-SSL mode, see below for usage issues)
|
||||

|
||||
|
||||
Admin Home Page:
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
### Method 1: Out-of-the-box
|
||||
|
||||
Only supports Windows, please go to the release to download, no need to install environment, just run `启动.bat` directly. Screenshots:
|
||||
|
||||

|
||||
|
||||
|
||||
### Method 2: Running the Code
|
||||
|
||||
```bash
|
||||
# Clone the code locally
|
||||
git clone https://github.com/kingmo888/rustdesk-api-server.git
|
||||
# Enter the directory
|
||||
cd rustdesk-api-server
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
# After ensuring dependencies are installed correctly, execute:
|
||||
# Please modify the port number yourself, it is recommended to keep 21114 as the default port for Rustdesk API
|
||||
python manage.py runserver 0.0.0.0:21114
|
||||
```
|
||||
|
||||
Now you can access it using `http://localhostIP:Port`.
|
||||
|
||||
**Note**: When configuring on CentOS, Django4 may have problems due to the low version of sqlite3 in the system. Please modify the file in the dependency library. Path: `xxxx/Lib/site-packages/django/db/backends/sqlite3/base.py` (Find the package address according to the situation), modify the content:
|
||||
```python
|
||||
# from sqlite3 import dbapi2 as Database #(comment out this line)
|
||||
from pysqlite3 import dbapi2 as Database # enable pysqlite3
|
||||
```
|
||||
|
||||
### Method 3: Docker Run
|
||||
|
||||
#### Docker Method 1: Build Yourself
|
||||
```bash
|
||||
git clone https://github.com/kingmo888/rustdesk-api-server.git
|
||||
cd rustdesk-api-server
|
||||
docker compose --compatibility up --build -d
|
||||
```
|
||||
Thanks to the enthusiastic netizen @ferocknew for providing.
|
||||
|
||||
#### Docker Method 2: Pre-built Run
|
||||
|
||||
docker run command:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name rustdesk-api-server \
|
||||
-p 21114:21114 \
|
||||
-e CSRF_TRUSTED_ORIGINS=http://yourdomain.com:21114 \ #Cross-origin trusted source, optional
|
||||
-e ID_SERVER=yourdomain.com \ #ID server used by the web control terminal
|
||||
-v /yourpath/db:/rustdesk-api-server/db \ #Modify /yourpath/db to your host database mount directory
|
||||
-v /etc/timezone:/etc/timezone:ro \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
--network bridge \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/kingmo888/rustdesk-api-server:latest
|
||||
```
|
||||
|
||||
docker-compose method:
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
rustdesk-api-server:
|
||||
container_name: rustdesk-api-server
|
||||
image: ghcr.io/kingmo888/rustdesk-api-server:latest
|
||||
environment:
|
||||
- CSRF_TRUSTED_ORIGINS=http://yourdomain.com:21114 #Cross-origin trusted source, optional
|
||||
- ID_SERVER=yourdomain.com #ID server used by the web control terminal
|
||||
volumes:
|
||||
- /yourpath/db:/rustdesk-api-server/db #Modify /yourpath/db to your host database mount directory
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "21114:21114"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable Name | Reference Value | Note |
|
||||
| ---- | ------- | ----------- |
|
||||
| `HOST` | Default `0.0.0.0` | IP binding of the service |
|
||||
| `TZ` | Default `Asia/Shanghai`, optional | Timezone |
|
||||
| `SECRET_KEY` | Optional, custom a random string | Program encryption key |
|
||||
| `CSRF_TRUSTED_ORIGINS` | Optional, verification off by default;<br>If you need to enable it, fill in your access address `http://yourdomain.com:21114` <br>**To disable verification, please delete this variable instead of leaving it blank** | Cross-origin trusted source |
|
||||
| `ID_SERVER` | Optional, default is the same host as the API server.<br>Customizable like `yourdomain.com` | ID server used by the web control terminal |
|
||||
| `DEBUG` | Optional, default `False` | Debug mode |
|
||||
| `ALLOW_REGISTRATION` | Optional, default `True` | Whether to allow new user registration |
|
||||
| Database Configuration | -- Start -- | If not using MYSQL, the following are unnecessary |
|
||||
| `DATABASE_TYPE` | Optional, default `SQLITE3` | Database type (SQLITE/MYSQL) |
|
||||
| `MYSQL_DBNAME` | Optional, default `-` | MYSQL database name |
|
||||
| `MYSQL_HOST` | Optional, default `127.0.0.1` | MYSQL database server IP |
|
||||
| `MYSQL_USER` | Optional, default `-` | MYSQL database username |
|
||||
| `MYSQL_PASSWORD` | Optional, default `-` | MYSQL database password |
|
||||
| `MYSQL_PORT` | Optional, default `3306` | MYSQL database port |
|
||||
| Database Configuration | -- End -- | See [sqlite3 migration to mysql tutorial](/tutorial/sqlite2mysql.md) |
|
||||
|
||||
## Usage Issues
|
||||
|
||||
- Administrator Settings
|
||||
|
||||
When there are no accounts in the database, the first registered account directly obtains super administrator privileges,
|
||||
|
||||
and subsequently registered accounts are ordinary accounts.
|
||||
|
||||
- Device Information
|
||||
|
||||
Tested, the client will send device information to the API interface regularly in the mode of installation as a service under non-green mode, so if you want device information, you need to install the Rustdesk client and start the service.
|
||||
|
||||
- Slow Connection Speed
|
||||
|
||||
The new version Key mode connection speed is slow. You can start the service on the server without the -k parameter. At this time, the client cannot configure the key either.
|
||||
|
||||
- Web Control Terminal Configuration
|
||||
|
||||
- Set the ID_SERVER environment variable or modify the ID_SERVER configuration item in the rustdesk_server_api/settings.py file and fill in the IP or domain name of the ID server/relay server.
|
||||
|
||||
- Web Control Terminal Keeps Spinning
|
||||
|
||||
- Check if the ID server filling is correct.
|
||||
|
||||
- The web control terminal currently only supports non-SSL mode. If the webui is accessed via https, remove the 's', otherwise ws cannot connect and keeps spinning. For example: https://domain.com/webui, change to http://domain.com/webui
|
||||
|
||||
- CSRF verification failed when logging in or logging out of backend operations. Request interrupted.
|
||||
|
||||
This operation is highly likely to be a combination of docker configuration + nginx reverse proxy + SSL. Pay attention to modifying CSRF_TRUSTED_ORIGINS. If it is SSL, it starts with https, otherwise it is http.
|
||||
|
||||
## Development Plans
|
||||
|
||||
- [x] Share devices with other registered users (v1.3+)
|
||||
|
||||
> Explanation: Similar to sharing URLs of network disks, the URL can be activated to obtain devices under a certain group or certain label.
|
||||
> Note: In fact, there is not much that can be done with the web API as middleware. More functions still need to be implemented by modifying the client, which is not very worthwhile.
|
||||
|
||||
- [x] Integration of Web client form (v1.4+)
|
||||
|
||||
> Integrating the great god's web client, already integrated. [Source](https://www.52pojie.cn/thread-1708319-1-1.html)
|
||||
|
||||
- [x] Filter expired (offline) devices to distinguish between online and offline devices (1.4.7)
|
||||
|
||||
> By configuration, clean or filter devices that have expired for more than a specified time.
|
||||
|
||||
- [x] Split the first screen into user list page and administrator list page and add pagination (1.4.6).
|
||||
|
||||
- [x] Support exporting information to xlsx files (1.4.6).
|
||||
|
||||
> Allows administrators to export all device information on the [All Devices] page.
|
||||
|
||||
- [x] Set whether to allow new user registration through configuration items (1.4.7).
|
||||
|
||||
- [x] Support mysql and sqlite3 migration to mysql (1.4.8).
|
||||
|
||||
## Other Related Tools
|
||||
|
||||
- [CMD script for modifying client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
|
||||
|
||||
- [rustdesk](https://github.com/rustdesk/rustdesk)
|
||||
|
||||
- [rustdesk-server](https://github.com/rustdesk/rustdesk-server)
|
||||
|
||||
## Stargazers over time
|
||||
[](https://starchart.cc/kingmo888/rustdesk-api-server)
|
0
api/__init__.py
Normal file
1
api/admin.py
Normal file
@@ -0,0 +1 @@
|
||||
from .admin_user import *
|
101
api/admin_user.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# cython:language_level=3
|
||||
from django.contrib import admin
|
||||
from api import models
|
||||
from django import forms
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class UserCreationForm(forms.ModelForm):
|
||||
"""A form for creating new users. Includes all the required
|
||||
fields, plus a repeated password."""
|
||||
password1 = forms.CharField(label=_('密码'), widget=forms.PasswordInput)
|
||||
password2 = forms.CharField(label=_('再次输入密码'), widget=forms.PasswordInput)
|
||||
|
||||
class Meta:
|
||||
model = models.UserProfile
|
||||
fields = ('username','is_active','is_admin')
|
||||
|
||||
def clean_password2(self):
|
||||
# Check that the two password entries match
|
||||
password1 = self.cleaned_data.get("password1")
|
||||
password2 = self.cleaned_data.get("password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise forms.ValidationError(_("密码校验失败,两次密码不一致。"))
|
||||
return password2
|
||||
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save the provided password in hashed format
|
||||
user = super(UserCreationForm, self).save(commit=False)
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class UserChangeForm(forms.ModelForm):
|
||||
"""A form for updating users. Includes all the fields on
|
||||
the user, but replaces the password field with admin's
|
||||
password hash display field.
|
||||
"""
|
||||
password = ReadOnlyPasswordHashField(label=(_("密码Hash值")), help_text=("<a href=\"../password/\">点击修改密码</a>."))
|
||||
class Meta:
|
||||
model = models.UserProfile
|
||||
fields = ('username', 'is_active', 'is_admin')
|
||||
|
||||
def clean_password(self):
|
||||
# Regardless of what the user provides, return the initial value.
|
||||
# This is done here, rather than on the field, because the
|
||||
# field does not have access to the initial value
|
||||
return self.initial["password"]
|
||||
#return self.initial["password"]
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save the provided password in hashed format
|
||||
user = super(UserChangeForm, self).save(commit=False)
|
||||
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
# The forms to add and change user instances
|
||||
form = UserChangeForm
|
||||
add_form = UserCreationForm
|
||||
password = ReadOnlyPasswordHashField(label=("Password HASH value"), help_text=("<a href=\"../password/\">Click to modify the password</a>."))
|
||||
# The fields to be used in displaying the User model.
|
||||
# These override the definitions on the base UserAdmin
|
||||
# that reference specific fields on auth.User.
|
||||
list_display = ('username', 'rid')
|
||||
list_filter = ('is_admin', 'is_active')
|
||||
fieldsets = (
|
||||
(_('基本信息'), {'fields': ('username', 'password', 'is_active', 'is_admin', 'rid', 'uuid', 'deviceInfo',)}),
|
||||
|
||||
)
|
||||
readonly_fields = ( 'rid', 'uuid')
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ('username', 'is_active', 'is_admin', 'password1', 'password2', )}
|
||||
),
|
||||
)
|
||||
|
||||
search_fields = ('username', )
|
||||
ordering = ('username',)
|
||||
filter_horizontal = ()
|
||||
|
||||
|
||||
admin.site.register(models.UserProfile, UserAdmin)
|
||||
admin.site.register(models.RustDeskToken, models.RustDeskTokenAdmin)
|
||||
admin.site.register(models.RustDeskTag, models.RustDeskTagAdmin)
|
||||
admin.site.register(models.RustDeskPeer, models.RustDeskPeerAdmin)
|
||||
admin.site.register(models.RustDesDevice, models.RustDesDeviceAdmin)
|
||||
admin.site.register(models.ShareLink, models.ShareLinkAdmin)
|
||||
admin.site.register(models.ConnLog, models.ConnLogAdmin)
|
||||
admin.site.register(models.FileLog, models.FileLogAdmin)
|
||||
admin.site.unregister(Group)
|
||||
admin.site.site_header = _('RustDesk自建Web')
|
||||
admin.site.site_title = _('未定义')
|
5
api/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = 'api'
|
102
api/forms.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from django import forms
|
||||
from api.models import UserProfile
|
||||
|
||||
class GenerateForm(forms.Form):
|
||||
#Platform
|
||||
platform = forms.ChoiceField(choices=[('windows','Windows'),('linux','Linux (currently unavailable)'),('android','Android (testing now available)')], initial='windows')
|
||||
version = forms.ChoiceField(choices=[('master','beta'),('1.3.2','1.3.2'),('1.3.1','1.3.1'),('1.3.0','1.3.0')], initial='1.3.2')
|
||||
delayFix = forms.BooleanField(initial=True, required=False)
|
||||
|
||||
#General
|
||||
exename = forms.CharField(label="Name for EXE file", required=True)
|
||||
appname = forms.CharField(label="Custom App Name", required=False)
|
||||
direction = forms.ChoiceField(widget=forms.RadioSelect, choices=[
|
||||
('incoming', 'Incoming Only'),
|
||||
('outgoing', 'Outgoing Only'),
|
||||
('both', 'Bidirectional')
|
||||
], initial='both')
|
||||
installation = forms.ChoiceField(label="Disable Installation", choices=[
|
||||
('installationY', 'No, enable installation'),
|
||||
('installationN', 'Yes, DISABLE installation')
|
||||
], initial='installationY')
|
||||
settings = forms.ChoiceField(label="Disable Settings", choices=[
|
||||
('settingsY', 'No, enable settings'),
|
||||
('settingsN', 'Yes, DISABLE settings')
|
||||
], initial='settingsY')
|
||||
|
||||
#Custom Server
|
||||
serverIP = forms.CharField(label="Host", required=False)
|
||||
apiServer = forms.CharField(label="API Server", required=False)
|
||||
key = forms.CharField(label="Key", required=False)
|
||||
urlLink = forms.CharField(label="Custom URL for links", required=False)
|
||||
|
||||
#Visual
|
||||
iconfile = forms.FileField(label="Custom App Icon (in .png format)", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}))
|
||||
logofile = forms.FileField(label="Custom App Logo (in .png format)", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}))
|
||||
theme = forms.ChoiceField(choices=[
|
||||
('light', 'Light'),
|
||||
('dark', 'Dark'),
|
||||
('system', 'Follow System')
|
||||
], initial='system')
|
||||
themeDorO = forms.ChoiceField(choices=[('default', 'Default'),('override', 'Override')], initial='default')
|
||||
|
||||
#Security
|
||||
passApproveMode = forms.ChoiceField(choices=[('password','Accept sessions via password'),('click','Accept sessions via click'),('password-click','Accepts sessions via both')],initial='password-click')
|
||||
permanentPassword = forms.CharField(widget=forms.PasswordInput(), required=False)
|
||||
runasadmin = forms.ChoiceField(choices=[('false','No'),('true','Yes')], initial='false')
|
||||
denyLan = forms.BooleanField(initial=False, required=False)
|
||||
enableDirectIP = forms.BooleanField(initial=False, required=False)
|
||||
#ipWhitelist = forms.BooleanField(initial=False, required=False)
|
||||
autoClose = forms.BooleanField(initial=False, required=False)
|
||||
|
||||
#Permissions
|
||||
permissionsDorO = forms.ChoiceField(choices=[('default', 'Default'),('override', 'Override')], initial='default')
|
||||
permissionsType = forms.ChoiceField(choices=[('custom', 'Custom'),('full', 'Full Access'),('view','Screen share')], initial='custom')
|
||||
enableKeyboard = forms.BooleanField(initial=True, required=False)
|
||||
enableClipboard = forms.BooleanField(initial=True, required=False)
|
||||
enableFileTransfer = forms.BooleanField(initial=True, required=False)
|
||||
enableAudio = forms.BooleanField(initial=True, required=False)
|
||||
enableTCP = forms.BooleanField(initial=True, required=False)
|
||||
enableRemoteRestart = forms.BooleanField(initial=True, required=False)
|
||||
enableRecording = forms.BooleanField(initial=True, required=False)
|
||||
enableBlockingInput = forms.BooleanField(initial=True, required=False)
|
||||
enableRemoteModi = forms.BooleanField(initial=False, required=False)
|
||||
|
||||
#Other
|
||||
removeWallpaper = forms.BooleanField(initial=True, required=False)
|
||||
|
||||
defaultManual = forms.CharField(widget=forms.Textarea, required=False)
|
||||
overrideManual = forms.CharField(widget=forms.Textarea, required=False)
|
||||
|
||||
class AddPeerForm(forms.Form):
|
||||
clientID = forms.CharField(label="Client Rustdesk ID", required=True)
|
||||
alias = forms.CharField(label="Client alias", required=True)
|
||||
tags = forms.CharField(label="Tags", required=False)
|
||||
username = forms.CharField(label="Username", required=False)
|
||||
hostname = forms.CharField(label="OS", required=False)
|
||||
platform = forms.CharField(label="Platform", required=False)
|
||||
ip = forms.CharField(label="IP", required=False)
|
||||
|
||||
class AssignPeerForm(forms.Form):
|
||||
uid = forms.ModelChoiceField(
|
||||
queryset=UserProfile.objects.all(),
|
||||
to_field_name='id',
|
||||
empty_label='Select a User',
|
||||
required=True
|
||||
)
|
||||
clientID = forms.CharField(label="Client Rustdesk ID", required=True)
|
||||
alias = forms.CharField(label="Client alias", required=True)
|
||||
tags = forms.CharField(label="Tags", required=False)
|
||||
username = forms.CharField(label="Username", required=False)
|
||||
hostname = forms.CharField(label="Hostname", required=False)
|
||||
platform = forms.CharField(label="Platform", required=False)
|
||||
ip = forms.CharField(label="IP", required=False)
|
||||
|
||||
class EditPeerForm(forms.Form):
|
||||
clientID = forms.CharField(label="Client Rustdesk ID", required=True)
|
||||
alias = forms.CharField(label="Client alias", required=True)
|
||||
tags = forms.CharField(label="Tags", required=False)
|
||||
username = forms.CharField(label="Username", required=False)
|
||||
hostname = forms.CharField(label="OS", required=False)
|
||||
platform = forms.CharField(label="Platform", required=False)
|
||||
ip = forms.CharField(label="IP", required=False)
|
78
api/front_locale.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
_('管理后台')
|
||||
_('ID列表')
|
||||
_('分享机器')
|
||||
_('这么简易的东西,忘记密码这功能就没必要了吧。')
|
||||
_('立即注册')
|
||||
_('创建时间')
|
||||
_('注册成功,请前往登录页登录。')
|
||||
_('注册日期')
|
||||
_('2、所分享的机器,被分享人享有相同的权限,如果机器设置了保存密码,被分享人也可以直接连接。')
|
||||
_('导出xlsx')
|
||||
_('生成分享链接')
|
||||
_('请输入8~20位密码。可以包含字母、数字和特殊字符。')
|
||||
_('尾页')
|
||||
_('请确认密码')
|
||||
_('注册')
|
||||
_('内存')
|
||||
_('首页')
|
||||
_('网页控制')
|
||||
_('注册时间')
|
||||
_('链接地址')
|
||||
_('请输入密码')
|
||||
_('系统用户名')
|
||||
_('状态')
|
||||
_('已有账号?立即登录')
|
||||
_('密码')
|
||||
_('别名')
|
||||
_('上一页')
|
||||
_('更新时间')
|
||||
_('综合屏')
|
||||
_('平台')
|
||||
_('全部用户')
|
||||
_('注册页')
|
||||
_('分享机器给其他用户')
|
||||
_('所有设备')
|
||||
_('连接密码')
|
||||
_('设备统计')
|
||||
_('所属用户')
|
||||
_('分享')
|
||||
_('请输入用户名')
|
||||
_('1、链接有效期为15分钟,切勿随意分享给他人。')
|
||||
_('CPU')
|
||||
_('客户端ID')
|
||||
_('下一页')
|
||||
_('登录')
|
||||
_('退出')
|
||||
_('请将要分享的机器调整到右侧')
|
||||
_('成功!如需分享,请复制以下链接给其他人:<br>')
|
||||
_('忘记密码?')
|
||||
_('计算机名')
|
||||
_('两次输入密码不一致!')
|
||||
_('页码')
|
||||
_('版本')
|
||||
_('用户名')
|
||||
_('3、为保障安全,链接有效期为15分钟、链接仅有效1次。链接一旦被(非分享人的登录用户)访问,分享生效,后续访问链接失效。')
|
||||
_('系统')
|
||||
_('我的机器')
|
||||
_('信息')
|
||||
_('远程ID')
|
||||
_('远程别名')
|
||||
_('用户ID')
|
||||
_('用户别名')
|
||||
_('用户IP')
|
||||
_('文件大小')
|
||||
_('发送/接受')
|
||||
_('记录于')
|
||||
_('连接开始时间')
|
||||
_('连接结束时间')
|
||||
_('时长')
|
||||
_('连接日志')
|
||||
_('文件传输日志')
|
||||
_('页码 #')
|
||||
_('下一页 #')
|
||||
_('上一页 #')
|
||||
_('第一页')
|
||||
_('上页')
|
243
api/migrations/0001_initial.py
Normal file
@@ -0,0 +1,243 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-14 12:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RustDesDevice",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"rid",
|
||||
models.CharField(blank=True, max_length=60, verbose_name="Client ID"),
|
||||
),
|
||||
("cpu", models.CharField(max_length=20, verbose_name="CPU")),
|
||||
("hostname", models.CharField(max_length=20, verbose_name="CPU name")),
|
||||
("memory", models.CharField(max_length=20, verbose_name="Memory")),
|
||||
("os", models.CharField(max_length=20, verbose_name="operating system")),
|
||||
("uuid", models.CharField(max_length=60, verbose_name="uuid")),
|
||||
(
|
||||
"username",
|
||||
models.CharField(blank=True, max_length=60, verbose_name="System username"),
|
||||
),
|
||||
("version", models.CharField(max_length=20, verbose_name="Client version")),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="Equipment registration time"),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(auto_now=True, verbose_name="Equipment update time"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "equipment",
|
||||
"verbose_name_plural": "Device List",
|
||||
"ordering": ("-rid",),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RustDeskPeer",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("uid", models.CharField(max_length=16, verbose_name="User ID")),
|
||||
("rid", models.CharField(max_length=60, verbose_name="Client ID")),
|
||||
("username", models.CharField(max_length=20, verbose_name="System username")),
|
||||
("hostname", models.CharField(max_length=30, verbose_name="Operating system name")),
|
||||
("alias", models.CharField(max_length=30, verbose_name="Alias")),
|
||||
("platform", models.CharField(max_length=30, verbose_name="platform")),
|
||||
("tags", models.CharField(max_length=30, verbose_name="Label")),
|
||||
("rhash", models.CharField(max_length=60, verbose_name="Device link password")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Peers",
|
||||
"verbose_name_plural": "PEERS list",
|
||||
"ordering": ("-username",),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RustDeskTag",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("uid", models.CharField(max_length=16, verbose_name="User ID")),
|
||||
("tag_name", models.CharField(max_length=60, verbose_name="Tag name")),
|
||||
(
|
||||
"tag_color",
|
||||
models.CharField(blank=True, max_length=60, verbose_name="Tag color"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Tags",
|
||||
"verbose_name_plural": "Tags list",
|
||||
"ordering": ("-uid",),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RustDeskToken",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("username", models.CharField(max_length=20, verbose_name="username")),
|
||||
("rid", models.CharField(max_length=16, verbose_name="RustDesk ID")),
|
||||
("uid", models.CharField(max_length=16, verbose_name="User ID")),
|
||||
("uuid", models.CharField(max_length=60, verbose_name="uuid")),
|
||||
(
|
||||
"access_token",
|
||||
models.CharField(
|
||||
blank=True, max_length=60, verbose_name="access_token"
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="Log in time"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Token",
|
||||
"verbose_name_plural": "Token list",
|
||||
"ordering": ("-username",),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ShareLink",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("uid", models.CharField(max_length=16, verbose_name="User ID")),
|
||||
("shash", models.CharField(max_length=60, verbose_name="Link Key")),
|
||||
("peers", models.CharField(max_length=20, verbose_name="Machine ID list")),
|
||||
("is_used", models.BooleanField(default=False, verbose_name="use or not")),
|
||||
("is_expired", models.BooleanField(default=False, verbose_name="Whether to expire")),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="Generation time"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Share link",
|
||||
"verbose_name_plural": "Link list",
|
||||
"ordering": ("-create_time",),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserProfile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(max_length=50, unique=True, verbose_name="username"),
|
||||
),
|
||||
("rid", models.CharField(max_length=16, verbose_name="RustDesk ID")),
|
||||
("uuid", models.CharField(max_length=60, verbose_name="uuid")),
|
||||
(
|
||||
"autoLogin",
|
||||
models.BooleanField(default=True, verbose_name="autoLogin"),
|
||||
),
|
||||
("rtype", models.CharField(max_length=20, verbose_name="rtype")),
|
||||
("deviceInfo", models.TextField(blank=True, verbose_name="login information:")),
|
||||
("is_active", models.BooleanField(default=True, verbose_name="Activate now")),
|
||||
("is_admin", models.BooleanField(default=False, verbose_name="Whether a administrator")),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "user list",
|
||||
"permissions": (
|
||||
("view_task", "Can see available tasks"),
|
||||
("change_task_status", "Can change the status of tasks"),
|
||||
("close_task", "Can remove a task by setting its status as closed"),
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.2.7 on 2024-02-21 10:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("api", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="cpu",
|
||||
field=models.CharField(max_length=100, verbose_name="CPU"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="hostname",
|
||||
field=models.CharField(max_length=100, verbose_name="主机名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="memory",
|
||||
field=models.CharField(max_length=100, verbose_name="内存"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="os",
|
||||
field=models.CharField(max_length=100, verbose_name="操作系统"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="username",
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name="系统用户名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="uuid",
|
||||
field=models.CharField(max_length=100, verbose_name="uuid"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="version",
|
||||
field=models.CharField(max_length=100, verbose_name="客户端版本"),
|
||||
),
|
||||
]
|
238
api/migrations/0003_alter_rustdesdevice_options_and_more.py
Normal file
@@ -0,0 +1,238 @@
|
||||
# Generated by Django 4.2.7 on 2024-03-15 20:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("api", "0002_alter_rustdesdevice_cpu_alter_rustdesdevice_hostname_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdesdevice",
|
||||
options={
|
||||
"ordering": ("-rid",),
|
||||
"verbose_name": "Device",
|
||||
"verbose_name_plural": "Device List",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdeskpeer",
|
||||
options={
|
||||
"ordering": ("-username",),
|
||||
"verbose_name": "Peers",
|
||||
"verbose_name_plural": "Peers List",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdesktag",
|
||||
options={
|
||||
"ordering": ("-uid",),
|
||||
"verbose_name": "Tags",
|
||||
"verbose_name_plural": "Tags List",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdesktoken",
|
||||
options={
|
||||
"ordering": ("-username",),
|
||||
"verbose_name": "Token",
|
||||
"verbose_name_plural": "Token List",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="sharelink",
|
||||
options={
|
||||
"ordering": ("-create_time",),
|
||||
"verbose_name": "Share Link",
|
||||
"verbose_name_plural": "Link List",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="userprofile",
|
||||
options={
|
||||
"permissions": (
|
||||
("view_task", "Can see available tasks"),
|
||||
("change_task_status", "Can change the status of tasks"),
|
||||
("close_task", "Can remove a task by setting its status as closed"),
|
||||
),
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "User List",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="create_time",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Device Registration Time"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="hostname",
|
||||
field=models.CharField(max_length=100, verbose_name="Hostname"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="memory",
|
||||
field=models.CharField(max_length=100, verbose_name="Memory"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="os",
|
||||
field=models.CharField(max_length=100, verbose_name="Operating System"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="rid",
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name="Client ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="username",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=100, verbose_name="System Username"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="version",
|
||||
field=models.CharField(max_length=100, verbose_name="Client Version"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="alias",
|
||||
field=models.CharField(max_length=30, verbose_name="Alias"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="hostname",
|
||||
field=models.CharField(max_length=30, verbose_name="Operating System Name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="platform",
|
||||
field=models.CharField(max_length=30, verbose_name="Platform"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="rhash",
|
||||
field=models.CharField(
|
||||
max_length=60, verbose_name="Device Connection Password"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="rid",
|
||||
field=models.CharField(max_length=60, verbose_name="Client ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="tags",
|
||||
field=models.CharField(max_length=30, verbose_name="Tag"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="User ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="username",
|
||||
field=models.CharField(max_length=20, verbose_name="System Username"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktag",
|
||||
name="tag_color",
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name="Tag Color"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktag",
|
||||
name="tag_name",
|
||||
field=models.CharField(max_length=60, verbose_name="Tag Name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktag",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="Belongs to User ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="access_token",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=60, verbose_name="Access Token"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="create_time",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="Login Time"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="User ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="username",
|
||||
field=models.CharField(max_length=20, verbose_name="Username"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="uuid",
|
||||
field=models.CharField(max_length=60, verbose_name="UUID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="create_time",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="Creation Time"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="is_expired",
|
||||
field=models.BooleanField(default=False, verbose_name="Is Expired"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="is_used",
|
||||
field=models.BooleanField(default=False, verbose_name="Is Used"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="peers",
|
||||
field=models.CharField(max_length=20, verbose_name="Machine ID List"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="shash",
|
||||
field=models.CharField(max_length=60, verbose_name="Link Key"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="User ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="deviceInfo",
|
||||
field=models.TextField(blank=True, verbose_name="Login Information:"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="is_active",
|
||||
field=models.BooleanField(default=True, verbose_name="Is Activated"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="is_admin",
|
||||
field=models.BooleanField(default=False, verbose_name="Is Admin"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="username",
|
||||
field=models.CharField(max_length=50, unique=True, verbose_name="Username"),
|
||||
),
|
||||
]
|
232
api/migrations/0004_alter_rustdesdevice_options_and_more.py
Normal file
@@ -0,0 +1,232 @@
|
||||
# Generated by Django 4.2.7 on 2024-03-15 23:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("api", "0003_alter_rustdesdevice_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdesdevice",
|
||||
options={
|
||||
"ordering": ("-rid",),
|
||||
"verbose_name": "设备",
|
||||
"verbose_name_plural": "设备列表",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdeskpeer",
|
||||
options={
|
||||
"ordering": ("-username",),
|
||||
"verbose_name": "Peers",
|
||||
"verbose_name_plural": "Peers列表",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdesktag",
|
||||
options={
|
||||
"ordering": ("-uid",),
|
||||
"verbose_name": "Tags",
|
||||
"verbose_name_plural": "Tags列表",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rustdesktoken",
|
||||
options={
|
||||
"ordering": ("-username",),
|
||||
"verbose_name": "Token",
|
||||
"verbose_name_plural": "Token列表",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="sharelink",
|
||||
options={
|
||||
"ordering": ("-create_time",),
|
||||
"verbose_name": "分享链接",
|
||||
"verbose_name_plural": "链接列表",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="userprofile",
|
||||
options={
|
||||
"permissions": (
|
||||
("view_task", "Can see available tasks"),
|
||||
("change_task_status", "Can change the status of tasks"),
|
||||
("close_task", "Can remove a task by setting its status as closed"),
|
||||
),
|
||||
"verbose_name": "用户",
|
||||
"verbose_name_plural": "用户列表",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="create_time",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="设备注册时间"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="hostname",
|
||||
field=models.CharField(max_length=100, verbose_name="主机名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="memory",
|
||||
field=models.CharField(max_length=100, verbose_name="内存"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="os",
|
||||
field=models.CharField(max_length=100, verbose_name="操作系统"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="rid",
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name="客户端ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="username",
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name="系统用户名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesdevice",
|
||||
name="version",
|
||||
field=models.CharField(max_length=100, verbose_name="客户端版本"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="alias",
|
||||
field=models.CharField(max_length=30, verbose_name="别名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="hostname",
|
||||
field=models.CharField(max_length=30, verbose_name="操作系统名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="platform",
|
||||
field=models.CharField(max_length=30, verbose_name="平台"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="rhash",
|
||||
field=models.CharField(max_length=60, verbose_name="设备链接密码"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="rid",
|
||||
field=models.CharField(max_length=60, verbose_name="客户端ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="tags",
|
||||
field=models.CharField(max_length=30, verbose_name="标签"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="用户ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdeskpeer",
|
||||
name="username",
|
||||
field=models.CharField(max_length=20, verbose_name="系统用户名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktag",
|
||||
name="tag_color",
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name="标签颜色"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktag",
|
||||
name="tag_name",
|
||||
field=models.CharField(max_length=60, verbose_name="标签名称"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktag",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="所属用户ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="access_token",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=60, verbose_name="access_token"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="create_time",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="登录时间"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="用户ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="username",
|
||||
field=models.CharField(max_length=20, verbose_name="用户名"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rustdesktoken",
|
||||
name="uuid",
|
||||
field=models.CharField(max_length=60, verbose_name="uuid"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="create_time",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="生成时间"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="is_expired",
|
||||
field=models.BooleanField(default=False, verbose_name="是否过期"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="is_used",
|
||||
field=models.BooleanField(default=False, verbose_name="是否使用"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="peers",
|
||||
field=models.CharField(max_length=20, verbose_name="机器ID列表"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="shash",
|
||||
field=models.CharField(max_length=60, verbose_name="链接Key"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sharelink",
|
||||
name="uid",
|
||||
field=models.CharField(max_length=16, verbose_name="用户ID"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="deviceInfo",
|
||||
field=models.TextField(blank=True, verbose_name="登录信息:"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="is_active",
|
||||
field=models.BooleanField(default=True, verbose_name="是否激活"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="is_admin",
|
||||
field=models.BooleanField(default=False, verbose_name="是否管理员"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="username",
|
||||
field=models.CharField(max_length=50, unique=True, verbose_name="用户名"),
|
||||
),
|
||||
]
|
253
api/migrations/0005_connlog_filelog_githubrun_and_more.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# Generated by Django 5.0.3 on 2024-10-28 10:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0004_alter_rustdesdevice_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConnLog',
|
||||
fields=[
|
||||
('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=20, null=True, verbose_name='Action')),
|
||||
('conn_id', models.CharField(max_length=10, null=True, verbose_name='Connection ID')),
|
||||
('from_ip', models.CharField(max_length=30, null=True, verbose_name='From IP')),
|
||||
('from_id', models.CharField(max_length=20, null=True, verbose_name='From ID')),
|
||||
('rid', models.CharField(max_length=20, null=True, verbose_name='To ID')),
|
||||
('conn_start', models.DateTimeField(null=True, verbose_name='Connected')),
|
||||
('conn_end', models.DateTimeField(null=True, verbose_name='Disconnected')),
|
||||
('session_id', models.CharField(max_length=60, null=True, verbose_name='Session ID')),
|
||||
('uuid', models.CharField(max_length=60, null=True, verbose_name='UUID')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FileLog',
|
||||
fields=[
|
||||
('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.CharField(max_length=500, verbose_name='Path')),
|
||||
('remote_id', models.CharField(default='0', max_length=20, verbose_name='Remote ID')),
|
||||
('user_id', models.CharField(default='0', max_length=20, verbose_name='User ID')),
|
||||
('user_ip', models.CharField(default='0', max_length=20, verbose_name='User IP')),
|
||||
('filesize', models.CharField(default='', max_length=500, verbose_name='Filesize')),
|
||||
('direction', models.IntegerField(default=0, verbose_name='Direction')),
|
||||
('logged_at', models.DateTimeField(null=True, verbose_name='Logged At')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GithubRun',
|
||||
fields=[
|
||||
('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.CharField(max_length=100, verbose_name='uuid')),
|
||||
('status', models.CharField(max_length=100, verbose_name='status')),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rustdesdevice',
|
||||
options={'ordering': ('-rid',), 'verbose_name': 'Device', 'verbose_name_plural': 'Device List'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rustdeskpeer',
|
||||
options={'ordering': ('-username',), 'verbose_name': 'Peers', 'verbose_name_plural': 'Peers List'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rustdesktag',
|
||||
options={'ordering': ('-uid',), 'verbose_name': 'Tags', 'verbose_name_plural': 'Tags List'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rustdesktoken',
|
||||
options={'ordering': ('-username',), 'verbose_name': 'Token', 'verbose_name_plural': 'Token List'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sharelink',
|
||||
options={'ordering': ('-create_time',), 'verbose_name': 'Share Link', 'verbose_name_plural': 'Link List'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='userprofile',
|
||||
options={'permissions': (('view_task', 'Can see available tasks'), ('change_task_status', 'Can change the status of tasks'), ('close_task', 'Can remove a task by setting its status as closed')), 'verbose_name': 'User', 'verbose_name_plural': 'User List'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rustdesdevice',
|
||||
name='ip',
|
||||
field=models.CharField(default='', max_length=16, verbose_name='IP Address'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rustdeskpeer',
|
||||
name='ip',
|
||||
field=models.CharField(blank=True, default='', max_length=16, verbose_name='IP Address'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='create_time',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Device Registration Time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='hostname',
|
||||
field=models.CharField(max_length=100, verbose_name='Hostname'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='memory',
|
||||
field=models.CharField(max_length=100, verbose_name='Memory'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='os',
|
||||
field=models.CharField(max_length=100, verbose_name='Operating System'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='rid',
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name='Client ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='update_time',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='设备更新时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='System Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesdevice',
|
||||
name='version',
|
||||
field=models.CharField(max_length=100, verbose_name='Client Version'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='alias',
|
||||
field=models.CharField(max_length=30, verbose_name='Alias'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='hostname',
|
||||
field=models.CharField(blank=True, max_length=30, verbose_name='Operating System Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='platform',
|
||||
field=models.CharField(blank=True, max_length=30, verbose_name='Platform'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='rhash',
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name='Device Connection Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='rid',
|
||||
field=models.CharField(max_length=60, verbose_name='Client ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='tags',
|
||||
field=models.CharField(blank=True, max_length=30, verbose_name='Tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='uid',
|
||||
field=models.CharField(max_length=16, verbose_name='User ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdeskpeer',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=20, verbose_name='System Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktag',
|
||||
name='tag_color',
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name='Tag Color'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktag',
|
||||
name='tag_name',
|
||||
field=models.CharField(max_length=60, verbose_name='Tag Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktag',
|
||||
name='uid',
|
||||
field=models.CharField(max_length=16, verbose_name='Belongs to User ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktoken',
|
||||
name='access_token',
|
||||
field=models.CharField(blank=True, max_length=60, verbose_name='Access Token'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktoken',
|
||||
name='create_time',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Login Time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktoken',
|
||||
name='uid',
|
||||
field=models.CharField(max_length=16, verbose_name='User ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktoken',
|
||||
name='username',
|
||||
field=models.CharField(max_length=20, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rustdesktoken',
|
||||
name='uuid',
|
||||
field=models.CharField(max_length=60, verbose_name='UUID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sharelink',
|
||||
name='create_time',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Generation Time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sharelink',
|
||||
name='is_expired',
|
||||
field=models.BooleanField(default=False, verbose_name='Is Expired'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sharelink',
|
||||
name='is_used',
|
||||
field=models.BooleanField(default=False, verbose_name='Is Used'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sharelink',
|
||||
name='peers',
|
||||
field=models.CharField(max_length=20, verbose_name='Machine ID List'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sharelink',
|
||||
name='shash',
|
||||
field=models.CharField(max_length=60, verbose_name='Link Key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sharelink',
|
||||
name='uid',
|
||||
field=models.CharField(max_length=16, verbose_name='User ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userprofile',
|
||||
name='deviceInfo',
|
||||
field=models.TextField(blank=True, verbose_name='Login Information:'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userprofile',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='Is Active'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userprofile',
|
||||
name='is_admin',
|
||||
field=models.BooleanField(default=False, verbose_name='Is Administrator'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userprofile',
|
||||
name='username',
|
||||
field=models.CharField(max_length=50, unique=True, verbose_name='Username'),
|
||||
),
|
||||
]
|
0
api/migrations/__init__.py
Normal file
2
api/models.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .models_work import *
|
||||
from .models_user import *
|
89
api/models_user.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# cython:language_level=3
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import (
|
||||
BaseUserManager,AbstractBaseUser,PermissionsMixin
|
||||
)
|
||||
from .models_work import *
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
class MyUserManager(BaseUserManager):
|
||||
def create_user(self, username, password=None):
|
||||
if not username:
|
||||
raise ValueError('Users must have an username')
|
||||
|
||||
user = self.model(username=username,
|
||||
)
|
||||
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, username, password):
|
||||
user = self.create_user(username,
|
||||
password=password,
|
||||
|
||||
)
|
||||
user.is_admin = True
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
|
||||
class UserProfile(AbstractBaseUser, PermissionsMixin):
|
||||
username = models.CharField(_('用户名'),
|
||||
unique=True,
|
||||
max_length=50)
|
||||
|
||||
rid = models.CharField(verbose_name='RustDesk ID', max_length=16)
|
||||
uuid = models.CharField(verbose_name='uuid', max_length=60)
|
||||
autoLogin = models.BooleanField(verbose_name='autoLogin', default=True)
|
||||
rtype = models.CharField(verbose_name='rtype', max_length=20)
|
||||
deviceInfo = models.TextField(verbose_name=_('登录信息:'), blank=True)
|
||||
|
||||
is_active = models.BooleanField(verbose_name=_('是否激活'), default=True)
|
||||
is_admin = models.BooleanField(verbose_name=_('是否管理员'), default=False)
|
||||
|
||||
objects = MyUserManager()
|
||||
|
||||
USERNAME_FIELD = 'username' # Fields used for household names
|
||||
REQUIRED_FIELDS = ['password'] #The field that must be filled in
|
||||
|
||||
|
||||
def get_full_name(self):
|
||||
# The user is identified by their email address
|
||||
return self.username
|
||||
|
||||
def get_short_name(self):
|
||||
# The user is identified by their email address
|
||||
return self.username
|
||||
|
||||
def __str__(self): # __unicode__ on Python 2
|
||||
return self.username
|
||||
|
||||
def has_perm(self, perm, obj=None): #Is there any specified permission
|
||||
"Does the user have a specific permission?"
|
||||
# Simplest possible answer: Yes, always
|
||||
return True
|
||||
|
||||
def has_module_perms(self, app_label):
|
||||
"Does the user have permissions to view the app `app_label`?"
|
||||
# Simplest possible answer: Yes, always
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
"Is the user a member of staff?"
|
||||
# Simplest possible answer: All admins are staff
|
||||
return self.is_admin
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("用户")
|
||||
verbose_name_plural = _("用户列表")
|
||||
permissions = (
|
||||
("view_task", "Can see available tasks"),
|
||||
("change_task_status", "Can change the status of tasks"),
|
||||
("close_task", "Can remove a task by setting its status as closed"),
|
||||
)
|
||||
|
151
api/models_work.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# cython:language_level=3
|
||||
from django.db import models
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
class RustDeskToken(models.Model):
|
||||
''' Token
|
||||
'''
|
||||
username = models.CharField(verbose_name=_('用户名'), max_length=20)
|
||||
rid = models.CharField(verbose_name=_('RustDesk ID'), max_length=16)
|
||||
uid = models.CharField(verbose_name=_('用户ID'), max_length=16)
|
||||
uuid = models.CharField(verbose_name=_('uuid'), max_length=60)
|
||||
access_token = models.CharField(verbose_name=_('access_token'), max_length=60, blank=True)
|
||||
create_time = models.DateTimeField(verbose_name=_('登录时间'), auto_now_add=True)
|
||||
#expire_time = models.DateTimeField(verbose_name='过期时间')
|
||||
class Meta:
|
||||
ordering = ('-username',)
|
||||
verbose_name = "Token"
|
||||
verbose_name_plural = _("Token列表")
|
||||
|
||||
class ConnLog(models.Model):
|
||||
id = models.IntegerField(verbose_name=_('ID'),primary_key=True)
|
||||
action = models.CharField(verbose_name=_('Action'), max_length=20, null=True)
|
||||
conn_id = models.CharField(verbose_name=_('Connection ID'), max_length=10, null=True)
|
||||
from_ip = models.CharField(verbose_name=_('From IP'), max_length=30, null=True)
|
||||
from_id = models.CharField(verbose_name=_('From ID'), max_length=20, null=True)
|
||||
rid = models.CharField(verbose_name=_('To ID'), max_length=20, null=True)
|
||||
conn_start = models.DateTimeField(verbose_name=_('Connected'), null=True)
|
||||
conn_end = models.DateTimeField(verbose_name=_('Disconnected'), null=True)
|
||||
session_id = models.CharField(verbose_name=_('Session ID'), max_length=60, null=True)
|
||||
uuid = models.CharField(verbose_name=_('uuid'), max_length=60, null=True)
|
||||
|
||||
class ConnLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'action', 'conn_id', 'from_ip', 'from_id', 'rid', 'conn_start', 'conn_end', 'session_id', 'uuid')
|
||||
search_fields = ('from_ip', 'rid')
|
||||
list_filter = ('id', 'from_ip', 'from_id', 'rid', 'conn_start', 'conn_end')
|
||||
|
||||
class FileLog(models.Model):
|
||||
id = models.IntegerField(verbose_name=_('ID'),primary_key=True)
|
||||
file = models.CharField(verbose_name=_('Path'), max_length=500)
|
||||
remote_id = models.CharField(verbose_name=_('Remote ID'), max_length=20, default='0')
|
||||
user_id = models.CharField(verbose_name=_('User ID'), max_length=20, default='0')
|
||||
user_ip = models.CharField(verbose_name=_('User IP'), max_length=20, default='0')
|
||||
filesize = models.CharField(verbose_name=_('Filesize'), max_length=500, default='')
|
||||
direction = models.IntegerField(verbose_name=_('Direction'), default=0)
|
||||
logged_at = models.DateTimeField(verbose_name=_('Logged At'), null=True)
|
||||
|
||||
class FileLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'file', 'remote_id', 'user_id', 'user_ip', 'filesize', 'direction', 'logged_at')
|
||||
search_fields = ('file', 'remote_id', 'user_id', 'user_ip')
|
||||
list_filter = ('id', 'file', 'remote_id', 'user_id', 'user_ip', 'filesize', 'direction', 'logged_at')
|
||||
|
||||
class RustDeskTokenAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'uid')
|
||||
search_fields = ('username', 'uid')
|
||||
list_filter = ('create_time', ) #filter
|
||||
|
||||
|
||||
class RustDeskTag(models.Model):
|
||||
''' Tags
|
||||
'''
|
||||
uid = models.CharField(verbose_name=_('所属用户ID'), max_length=16)
|
||||
tag_name = models.CharField(verbose_name=_('标签名称'), max_length=60)
|
||||
tag_color = models.CharField(verbose_name=_('标签颜色'), max_length=60, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-uid',)
|
||||
verbose_name = "Tags"
|
||||
verbose_name_plural = _("Tags列表")
|
||||
|
||||
class RustDeskTagAdmin(admin.ModelAdmin):
|
||||
list_display = ('tag_name', 'uid', 'tag_color')
|
||||
search_fields = ('tag_name', 'uid')
|
||||
list_filter = ('uid', )
|
||||
|
||||
|
||||
class RustDeskPeer(models.Model):
|
||||
''' Pees
|
||||
'''
|
||||
uid = models.CharField(verbose_name=_('用户ID'), max_length=16)
|
||||
rid = models.CharField(verbose_name=_('客户端ID'), max_length=60)
|
||||
username = models.CharField(verbose_name=_('系统用户名'), max_length=20, blank=True)
|
||||
hostname = models.CharField(verbose_name=_('操作系统名'), max_length=30, blank=True)
|
||||
alias = models.CharField(verbose_name=_('别名'), max_length=30)
|
||||
platform = models.CharField(verbose_name=_('平台'), max_length=30, blank=True)
|
||||
tags = models.CharField(verbose_name=_('标签'), max_length=30, blank=True)
|
||||
rhash = models.CharField(verbose_name=_('设备链接密码'), max_length=60, blank=True)
|
||||
ip = models.CharField(verbose_name=_('IP Address'), max_length=16, default="", blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-username',)
|
||||
verbose_name = "Peers"
|
||||
verbose_name_plural = _("Peers列表" )
|
||||
|
||||
|
||||
class RustDeskPeerAdmin(admin.ModelAdmin):
|
||||
list_display = ('rid', 'uid', 'username', 'hostname', 'platform', 'alias', 'tags', 'ip')
|
||||
search_fields = ('deviceid', 'alias')
|
||||
list_filter = ('rid', 'uid', )
|
||||
|
||||
|
||||
class RustDesDevice(models.Model):
|
||||
rid = models.CharField(verbose_name=_('客户端ID'), max_length=60, blank=True)
|
||||
cpu = models.CharField(verbose_name='CPU', max_length=100)
|
||||
hostname = models.CharField(verbose_name=_('主机名'), max_length=100)
|
||||
memory = models.CharField(verbose_name=_('内存'), max_length=100)
|
||||
os = models.CharField(verbose_name=_('操作系统'), max_length=100)
|
||||
uuid = models.CharField(verbose_name='uuid', max_length=100)
|
||||
username = models.CharField(verbose_name=_('系统用户名'), max_length=100, blank=True)
|
||||
version = models.CharField(verbose_name=_('客户端版本'), max_length=100)
|
||||
create_time = models.DateTimeField(verbose_name=_('设备注册时间'), auto_now_add=True)
|
||||
update_time = models.DateTimeField(verbose_name=('设备更新时间'), auto_now=True, blank=True)
|
||||
ip = models.CharField(verbose_name=_('IP Address'), max_length=16, default="")
|
||||
|
||||
class Meta:
|
||||
ordering = ('-rid',)
|
||||
verbose_name = _("设备")
|
||||
verbose_name_plural = _("设备列表" )
|
||||
|
||||
class RustDesDeviceAdmin(admin.ModelAdmin):
|
||||
list_display = ('rid', 'hostname', 'memory', 'uuid', 'version', 'create_time', 'update_time', 'ip')
|
||||
search_fields = ('hostname', 'memory')
|
||||
list_filter = ('rid', )
|
||||
|
||||
class ShareLink(models.Model):
|
||||
''' Share link
|
||||
'''
|
||||
uid = models.CharField(verbose_name=_('用户ID'), max_length=16)
|
||||
shash = models.CharField(verbose_name=_('链接Key'), max_length=60)
|
||||
peers = models.CharField(verbose_name=_('机器ID列表'), max_length=20)
|
||||
is_used = models.BooleanField(verbose_name=_('是否使用'), default=False)
|
||||
is_expired = models.BooleanField(verbose_name=_('是否过期'), default=False)
|
||||
create_time = models.DateTimeField(verbose_name=_('生成时间'), auto_now_add=True)
|
||||
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ('-create_time',)
|
||||
verbose_name = _("分享链接")
|
||||
verbose_name_plural = _("链接列表" )
|
||||
|
||||
|
||||
class ShareLinkAdmin(admin.ModelAdmin):
|
||||
list_display = ('shash', 'uid', 'peers', 'is_used', 'is_expired', 'create_time')
|
||||
search_fields = ('peers', )
|
||||
list_filter = ('is_used', 'uid', 'is_expired' )
|
||||
|
||||
class GithubRun(models.Model):
|
||||
id = models.IntegerField(verbose_name="ID",primary_key=True)
|
||||
uuid = models.CharField(verbose_name="uuid", max_length=100)
|
||||
status = models.CharField(verbose_name="status", max_length=100)
|
28
api/templates/add_peer.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk{% endblock %}
|
||||
{% block legend_name %}{{ "Add Peer" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
<div style="padding: 20px; background-color: #F2F2F2;">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md15">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{{ "Add Client" }}</div>
|
||||
<div class="layui-card-body">
|
||||
<form action="/api/add_peer" method="post" enctype="multipart/form-data">
|
||||
<label for="{{ form.clientID.id_for_label }}">Rustdesk client ID:</label>
|
||||
{{ form.clientID }}<br><br>
|
||||
<label for="{{ form.alias.id_for_label }}">Alias:</label>
|
||||
{{ form.alias }}<br><br>
|
||||
<label for="{{ form.tags.id_for_label }}">Tags:</label>
|
||||
{{ form.tags }}<br><br>
|
||||
<button type="submit">Add Client</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("{{ form.clientID.id_for_label }}").value = "{{rid}}"
|
||||
</script>
|
||||
{% endblock %}
|
29
api/templates/assign_peer.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk{% endblock %}
|
||||
{% block legend_name %}{{ "Add Peer" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
<div style="padding: 20px; background-color: #F2F2F2;">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md15">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{{ "Assign Client to User" }}</div>
|
||||
<div class="layui-card-body">
|
||||
<form action="/api/assign_peer" method="post" enctype="multipart/form-data">
|
||||
{{ form.uid }}<br><br>
|
||||
<label for="{{ form.clientID.id_for_label }}">Rustdesk client ID:</label>
|
||||
{{ form.clientID }}<br><br>
|
||||
<label for="{{ form.alias.id_for_label }}">Alias:</label>
|
||||
{{ form.alias }}<br><br>
|
||||
<label for="{{ form.tags.id_for_label }}">Tags:</label>
|
||||
{{ form.tags }}<br><br>
|
||||
<button type="submit">Assign Client</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("{{ form.clientID.id_for_label }}").value = "{{rid}}"
|
||||
</script>
|
||||
{% endblock %}
|
75
api/templates/base.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
|
||||
{% block link %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src={% static "layui/layui.js" %}></script>
|
||||
<script>
|
||||
layui.use('element', function () {
|
||||
var element = layui.element; //导航的hover效果、二级菜单等功能,需要依赖element模块
|
||||
|
||||
//监听导航点击
|
||||
element.on('nav(demo)', function (elem) {
|
||||
//console.log(elem)
|
||||
layer.msg(elem.text());
|
||||
});
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Place your globalSearch function or any DOM-related JS here
|
||||
window.globalSearch = function() {
|
||||
var input, filter, tables, tr, td, txtValue;
|
||||
input = document.getElementById("globalSearchInput");
|
||||
filter = input.value.toUpperCase();
|
||||
tables = document.getElementsByTagName("table");
|
||||
|
||||
for (let table of tables) {
|
||||
tr = table.getElementsByTagName("tr");
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
let row = tr[i];
|
||||
let cells = row.getElementsByTagName("td");
|
||||
let textContent = Array.from(cells).map(cell => cell.textContent.toUpperCase());
|
||||
row.style.display = textContent.some(text => text.includes(filter)) ? "" : "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<ul class="layui-nav">
|
||||
<li class="layui-nav-item"><a href="/">User Devices</a></li>
|
||||
{% if u.is_admin %}
|
||||
<li class="layui-nav-item"><a href="/api/work?show_type=admin">{% trans "所有设备" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<!-- <li class="layui-nav-item"><a href="/api/share">{% trans "分享" %}</a></li> -->
|
||||
<li class="layui-nav-item"><a href="/webui" target="_blank">{% trans "网页控制" %}</a></li>
|
||||
|
||||
{% if u.is_admin %}
|
||||
<li class="layui-nav-item"><a href="/admin" target="_blank">{% trans "管理后台" %}</a>
|
||||
</li>
|
||||
<li class="layui-nav-item"><a href="/api/conn_log">Connection Log</a></li>
|
||||
<li class="layui-nav-item"><a href="/api/file_log">File Transfer Log</a></li>
|
||||
<li class="layui-nav-item"><a href="/api/sys_info">Server Information</a></li>
|
||||
<li class="layui-nav-item"><a href="/api/clients">Client Downloads</a></li>
|
||||
{% endif %}
|
||||
<li class="layui-nav-item"><a href="/api/user_action?action=logout" target="_blank">{% trans "退出" %}</a></li>
|
||||
</ul>
|
||||
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
|
||||
<legend>{% block legend_name %}{% endblock %}</legend>
|
||||
</fieldset>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
84
api/templates/base_phone.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
|
||||
{% block link %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src={% static "layui/layui.js" %}></script>
|
||||
<script>
|
||||
layui.use('element', function () {
|
||||
var element = layui.element; //导航的hover效果、二级菜单等功能,需要依赖element模块
|
||||
|
||||
//监听导航点击
|
||||
element.on('nav(demo)', function (elem) {
|
||||
//console.log(elem)
|
||||
layer.msg(elem.text());
|
||||
});
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Place your globalSearch function or any DOM-related JS here
|
||||
window.globalSearch = function() {
|
||||
var input, filter, tables, tr, td, txtValue;
|
||||
input = document.getElementById("globalSearchInput");
|
||||
filter = input.value.toUpperCase();
|
||||
tables = document.getElementsByTagName("table");
|
||||
|
||||
for (let table of tables) {
|
||||
tr = table.getElementsByTagName("tr");
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
let row = tr[i];
|
||||
let cells = row.getElementsByTagName("td");
|
||||
let textContent = Array.from(cells).map(cell => cell.textContent.toUpperCase());
|
||||
row.style.display = textContent.some(text => text.includes(filter)) ? "" : "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div id="container">
|
||||
<nav role="navigation">
|
||||
<div id="menuToggle">
|
||||
<input type="checkbox" />
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<ul id="menu">
|
||||
<li class="layui-nav-item"><a href="/">User Devices</a></li>
|
||||
{% if u.is_admin %}
|
||||
<li class="layui-nav-item"><a href="/api/work?show_type=admin">{% trans "所有设备" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<!-- <li class="layui-nav-item"><a href="/api/share">{% trans "分享" %}</a></li> -->
|
||||
<li class="layui-nav-item"><a href="/webui" target="_blank">{% trans "网页控制" %}</a></li>
|
||||
|
||||
{% if u.is_admin %}
|
||||
<li class="layui-nav-item"><a href="/admin" target="_blank">{% trans "管理后台" %}</a>
|
||||
</li>
|
||||
<li class="layui-nav-item"><a href="/api/conn_log">Connection Log</a></li>
|
||||
<li class="layui-nav-item"><a href="/api/file_log">File Transfer Log</a></li>
|
||||
<li class="layui-nav-item"><a href="/api/sys_info">Server Information</a></li>
|
||||
<li class="layui-nav-item"><a href="/api/clients">Client Downloads</a></li>
|
||||
{% endif %}
|
||||
<li class="layui-nav-item"><a href="/api/user_action?action=logout" target="_blank">{% trans "退出" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<br>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
|
||||
<legend>{% block legend_name %}{% endblock %}</legend>
|
||||
</fieldset>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
89
api/templates/clients.html
Normal file
@@ -0,0 +1,89 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "Client Downloads" | translate }}{% endblock %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<script src="{% static 'js/sorttable.js' %}"></script>
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
grid-template-rows: repeat(2, auto); /* Adjust the number of rows as needed */
|
||||
padding: 20px; /* Adjust the padding value as needed */
|
||||
max-width: 1000px; /* Set a maximum width for the container */
|
||||
margin: 0 auto; /* Center the container horizontally */
|
||||
}
|
||||
.section {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Add a subtle box shadow */
|
||||
border-radius: 5px; /* Add rounded corners for a more 3D effect */
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Place your globalSearch function or any DOM-related JS here
|
||||
window.globalSearch = function() {
|
||||
var input, filter, tables, tr, td, txtValue;
|
||||
input = document.getElementById("globalSearchInput");
|
||||
filter = input.value.toUpperCase();
|
||||
tables = document.getElementsByTagName("table");
|
||||
|
||||
for (let table of tables) {
|
||||
tr = table.getElementsByTagName("tr");
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
let row = tr[i];
|
||||
let cells = row.getElementsByTagName("td");
|
||||
let textContent = Array.from(cells).map(cell => cell.textContent.toUpperCase());
|
||||
row.style.display = textContent.some(text => text.includes(filter)) ? "" : "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
|
||||
<a href="/api/generator">Client Generator</a>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h1>Github Clients</h1>
|
||||
<table class="sortable">
|
||||
<thead></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
{% for filename, fileinfo in client_files.items %}
|
||||
<tr>
|
||||
<td><a href='/api/download?filename={{filename}}&path={{fileinfo.path}}'>{{filename}}</a></td>
|
||||
<td>{{fileinfo.modified}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h1>Custom Clients</h1>
|
||||
<table class="sortable">
|
||||
<thead></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
{% for filename, fileinfo in client_custom_files.items %}
|
||||
<tr>
|
||||
<td><a href='/api/download?filename={{filename}}&path={{fileinfo.path}}'>{{filename}}</a></td>
|
||||
<td>{{fileinfo.modified}}</td>
|
||||
<td><a href='/api/delete_file?filename={{filename}}&path={{fileinfo.path}}'>Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
32
api/templates/edit_peer.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk{% endblock %}
|
||||
{% block legend_name %}{{ "Edit Peer" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
{{peer.rid}}
|
||||
<div style="padding: 20px; background-color: #F2F2F2;">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md15">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{{ "Edit Peer" }} {{ peer.rid }}</div>
|
||||
<div class="layui-card-body">
|
||||
<form action="/api/edit_peer" method="post" enctype="multipart/form-data">
|
||||
<label for="{{ form.clientID.id_for_label }}">Client Rustdesk ID:</label>
|
||||
{{ form.clientID }}<br><br>
|
||||
<label for="{{ form.alias.id_for_label }}">Alias:</label>
|
||||
{{ form.alias }}<br><br>
|
||||
<label for="{{ form.tags.id_for_label }}">Tags:</label>
|
||||
{{ form.tags }}<br><br>
|
||||
<label for="{{ form.username.id_for_label }}">System Username:</label>
|
||||
{{ form.username }}<br><br>
|
||||
<label for="{{ form.hostname.id_for_label }}">Computer Name:</label>
|
||||
{{ form.hostname }}<br><br>
|
||||
<label for="{{ form.platform.id_for_label }}">Platform:</label>
|
||||
{{ form.platform }}<br><br>
|
||||
<button type="submit">Save Client</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
7
api/templates/generated.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "Client Generator" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
<a href='/api/download_client?filename={{filename}}&uuid={{uuid}}'>{{filename}}</a>
|
||||
{% endblock %}
|
253
api/templates/generator.html
Normal file
@@ -0,0 +1,253 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "Client Generator" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background-color: #000;
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.platform {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 20px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr; /* Adjust as needed */
|
||||
grid-gap: 20px;
|
||||
margin: 0 auto; /* Center the container horizontally */
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
.column {
|
||||
flex: 50%;
|
||||
}
|
||||
h1 {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
h2 {
|
||||
color: #fff;
|
||||
margin-top: 0;
|
||||
}
|
||||
.section {
|
||||
background-color: #111;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
flex: 50%;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #bbb;
|
||||
}
|
||||
input[type="text"], input[type="password"], select, textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #222;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="radio"], input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
button {
|
||||
background-color: #0077ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0066cc;
|
||||
}
|
||||
.platform-icons {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.platform-icon {
|
||||
font-size: 32px;
|
||||
color: #bbb;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.platform-icon:hover, .platform-icon.active {
|
||||
color: #fff;
|
||||
}
|
||||
.checkbox-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><i class="fas fa-cogs"></i> RustDesk Custom Client Builder</h1>
|
||||
<form action="/api/generator" method="post" enctype="multipart/form-data">
|
||||
<div class="platform">
|
||||
<h2><i class="fas fa-desktop"></i> Select Platform</h2>
|
||||
<div class="platform-icons">
|
||||
<i class="fab fa-windows platform-icon active" data-platform="windows"></i>
|
||||
<i class="fab fa-linux platform-icon" data-platform="linux"></i>
|
||||
<i class="fab fa-android platform-icon" data-platform="android"></i>
|
||||
</div>
|
||||
<select name="platform" id="id_platform">
|
||||
<option value="windows" selected>Windows</option>
|
||||
<option value="linux">Linux</option>
|
||||
<option value="android">Android</option>
|
||||
</select>
|
||||
<label for="{{ form.version.id_for_label }}">Rustdesk Version:</label>
|
||||
{{ form.version }}
|
||||
<label for="{{ form.delayFix.id_for_label }}">{{ form.delayFix }} Fix connection delay when using third-party API</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h2><i class="fas fa-sliders-h"></i> General</h2>
|
||||
<label for="{{ form.exename.id_for_label }}">Name of the configuration:</label>
|
||||
{{ form.exename }}<br><br>
|
||||
<label for="{{ form.appname.id_for_label }}">Custom Application Name:</label>
|
||||
{{ form.appname }}<br><br>
|
||||
<label for="{{ form.direction.id_for_label }}">Connection Type:</label>
|
||||
{{ form.direction }}<br><br>
|
||||
<label for="{{ form.installation.id_for_label }}">Disable Installation:</label>
|
||||
{{ form.installation }}<br><br>
|
||||
<label for="{{ form.settings.id_for_label }}">Disable Settings:</label>
|
||||
{{ form.settings }}<br><br>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><i class="fas fa-server"></i> Custom Server</h2>
|
||||
<label for="{{ form.serverIP.id_for_label }}">Host:</label>
|
||||
{{ form.serverIP }}<br><br>
|
||||
<label for="{{ form.key.id_for_label }}">Key:</label>
|
||||
{{ form.key }}<br><br>
|
||||
<label for="{{ form.apiServer.id_for_label }}">API:</label>
|
||||
{{ form.apiServer }}<br><br>
|
||||
<label for="{{ form.urlLink.id_for_label }}">Custom URL for links (replaces https://rustdesk.com):</label>
|
||||
{{ form.urlLink }}<br><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h2><i class="fas fa-shield-alt"></i> Security</h2>
|
||||
<label for="{{ form.runasadmin.id_for_label }}">Always run as Administrator?</label>
|
||||
{{ form.runasadmin }}<br><br>
|
||||
<label for="{{ form.passApproveMode.id_for_label }}">Password Approve mode:</label>
|
||||
{{ form.passApproveMode }}<br><br>
|
||||
<label for="{{ form.permanentPassword.id_for_label }}">Set Permanent Password:</label>
|
||||
{{ form.permanentPassword }} *The password is used as default, but can be changed by the client<br><br>
|
||||
|
||||
|
||||
<label for="{{ form.denyLan.id_for_label }}">{{ form.denyLan }} Deny LAN discovery</label><br>
|
||||
|
||||
<label for="{{ form.enableDirectIP.id_for_label }}">{{ form.enableDirectIP }} Enable direct IP access</label><br>
|
||||
|
||||
<label for="{{ form.autoClose.id_for_label }}">{{ form.autoClose }} Automatically close incoming sessions on user inactivity</label><br>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><i class="fas fa-paint-brush"></i> Visual</h2>
|
||||
<label for="{{ form.iconfile.id_for_label }}">Custom App Icon (in .png format)</label>
|
||||
{{ form.iconfile }}<br><br>
|
||||
<!-- <input type="file" name="iconfile" id="iconfile" accept="image/png"> -->
|
||||
<div id="icon-preview"></div><br><br>
|
||||
<label for="{{ form.logofile.id_for_label }}">Custom App Logo (in .png format)</label>
|
||||
{{ form.logofile }}<br><br>
|
||||
<!-- <input type="file" name="logofile" id="logofile" accept="image/png"> -->
|
||||
<div id="logo-preview"></div><br><br>
|
||||
<label for="{{ form.theme.id_for_label }}">Theme:</label>
|
||||
{{ form.theme }} {{ form.themeDorO }} *Default sets the theme but allows the client to change it, Override sets the theme permanently.<br><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h2><i class="fas fa-lock"></i> Permissions</h2>
|
||||
The following Permissions can be set as default (the user can change the settins) or override (the settings cannot be changed).<br>
|
||||
{{ form.permissionsDorO }}
|
||||
<label for="{{ form.permissionsType.id_for_label }}">Permission type:</label>
|
||||
{{ form.permissionsType }}<br><br>
|
||||
<div class="checkbox-group">
|
||||
<label for="{{ form.enableKeyboard.id_for_label }}">{{ form.enableKeyboard }} Enable keyboard/mouse</label>
|
||||
<label for="{{ form.enableClipboard.id_for_label }}">{{ form.enableClipboard }} Enable clipboard</label>
|
||||
<label for="{{ form.enableFileTransfer.id_for_label }}">{{ form.enableFileTransfer }} Enable file transfer</label>
|
||||
<label for="{{ form.enableAudio.id_for_label }}">{{ form.enableAudio }} Enable audio</label>
|
||||
<label for="{{ form.enableTCP.id_for_label }}">{{ form.enableTCP }} Enable TCP tunneling</label>
|
||||
<label for="{{ form.enableRemoteRestart.id_for_label }}">{{ form.enableRemoteRestart }} Enable remote restart</label>
|
||||
<label for="{{ form.enableRecording.id_for_label }}">{{ form.enableRecording }} Enable recording session</label>
|
||||
<label for="{{ form.enableBlockingInput.id_for_label }}">{{ form.enableBlockingInput }} Enable blocking user input</label>
|
||||
<label for="{{ form.enableRemoteModi.id_for_label }}">{{ form.enableRemoteModi }} Enable remote configuration modification</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><i class="fas fa-cog"></i> Other</h2>
|
||||
<label for="{{ form.removeWallpaper.id_for_label }}">{{ form.removeWallpaper }} Remove wallpaper during incoming sessions</label><br>
|
||||
<label for="{{ form.defaultManual.id_for_label }}">Default settings</label><br>
|
||||
{{ form.defaultManual }}<br><br>
|
||||
<label for="{{ form.overrideManual.id_for_label }}">Override settings</label><br>
|
||||
{{ form.overrideManual }}<br><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="platform">
|
||||
<div class="section">
|
||||
<button type="submit"><i class="fas fa-rocket"></i> Generate Custom Client</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.querySelectorAll('.platform-icon').forEach(icon => {
|
||||
icon.addEventListener('click', function() {
|
||||
document.querySelectorAll('.platform-icon').forEach(i => i.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
document.getElementById('id_platform').value = this.dataset.platform;
|
||||
});
|
||||
});
|
||||
document.getElementById("{{ form.iconfile.id_for_label }}").addEventListener('change', function(event) {
|
||||
previewImage(event.target, 'icon-preview');
|
||||
});
|
||||
document.getElementById("{{ form.logofile.id_for_label }}").addEventListener('change', function(event) {
|
||||
previewImage(event.target, 'logo-preview');
|
||||
});
|
||||
function previewImage(input, previewContainerId) {
|
||||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
img.style.maxWidth = '300px';
|
||||
img.style.maxHeight = '60px';
|
||||
document.getElementById(previewContainerId).innerHTML = '';
|
||||
document.getElementById(previewContainerId).appendChild(img);
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
70
api/templates/login.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% load static %}
|
||||
{% load my_filters %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>{{ "登录" | translate }}_【RustDeskWeb】</title>
|
||||
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'layui/css/style.css' %}">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-main">
|
||||
<header class="layui-elip">{{ "登录" | translate }}</header>
|
||||
<form class="layui-form">
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="account" required lay-verify="required" placeholder="{{ "用户名" | translate }}" autocomplete="off"
|
||||
class="layui-input">
|
||||
</div>
|
||||
<div class="layui-input-inline">
|
||||
<input type="password" name="password" required lay-verify="required" placeholder="{{ "密码" | translate }}" autocomplete="off"
|
||||
class="layui-input">
|
||||
</div>
|
||||
<div class="layui-input-inline login-btn">
|
||||
<button lay-submit lay-filter="login" class="layui-btn">{{ "登录" | translate }}</button>
|
||||
</div>
|
||||
<hr/>
|
||||
<p><a href="/api/user_action?action=register" class="fl">{{ "立即注册" | translate }}</a><a href="javascript:;" class="fr">{{ "忘记密码?" | translate }}</a></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<script src={% static "layui/layui.js" %}></script>
|
||||
<script type="text/javascript">
|
||||
layui.use(['form','layer','jquery'], function () {
|
||||
|
||||
// 操作对象
|
||||
var form = layui.form;
|
||||
var $ = layui.jquery;
|
||||
form.on('submit(login)',function (data) {
|
||||
console.log(data.field);
|
||||
$.ajax({
|
||||
url:'/api/user_action?action=login',
|
||||
data:data.field,
|
||||
dataType:'json',
|
||||
type:'post',
|
||||
|
||||
success: function(resp) {
|
||||
if(resp.code==1) {
|
||||
//layer.alert(resp.msg,{icon:1});
|
||||
location.href = resp.url;
|
||||
} else {
|
||||
layer.alert(resp.msg,{icon:5});
|
||||
}
|
||||
}
|
||||
})
|
||||
return false;
|
||||
})
|
||||
$('.fr').on('click', function(){
|
||||
layer.alert("{{ "这么简易的东西,忘记密码这功能就没必要了吧。" | translate }}",{icon:5});
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
13
api/templates/msg.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% load my_filters %}
|
||||
{% block title %}{{title}}{% endblock %}
|
||||
{% block legend_name %}{{ "信息" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
<div style="padding: 20px; background-color: #F2F2F2;">
|
||||
<div class="layui-row layui-col-space15">
|
||||
{% autoescape off %}
|
||||
{{msg}}
|
||||
{% endautoescape %}
|
||||
</div></div>
|
||||
|
||||
{% endblock %}
|
144
api/templates/reg.html
Normal file
@@ -0,0 +1,144 @@
|
||||
{% load static %}
|
||||
{% load my_filters %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{{ "注册" | translate }}_【RustDeskWeb】</title>
|
||||
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'layui/css/style.css' %}">
|
||||
<link rel="icon" href="../frame/static/image/code.png">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-main">
|
||||
<header class="layui-elip" style="width: 82%">{{ "注册页" | translate }}</header>
|
||||
|
||||
<!-- 表单选项 -->
|
||||
<form class="layui-form">
|
||||
<div class="layui-input-inline">
|
||||
<!-- 用户名 -->
|
||||
<div class="layui-inline" style="width: 85%">
|
||||
<input type="text" id="user" name="account" required lay-verify="required" placeholder="{{ "请输入用户名" | translate }}" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<!-- 对号 -->
|
||||
<div class="layui-inline">
|
||||
<i class="layui-icon" id="ri" style="color: green;font-weight: bolder;" hidden></i>
|
||||
</div>
|
||||
<!-- 错号 -->
|
||||
<div class="layui-inline">
|
||||
<i class="layui-icon" id="wr" style="color: red; font-weight: bolder;" hidden>ဆ</i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 密码 -->
|
||||
<div class="layui-input-inline">
|
||||
<div class="layui-inline" style="width: 85%">
|
||||
<input type="password" id="pwd" name="password" required lay-verify="required" placeholder="{{ "请输入密码" | translate }}" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<!-- Signs -->
|
||||
<div class="layui-inline">
|
||||
<i class="layui-icon" id="pri" style="color: green;font-weight: bolder;" hidden></i>
|
||||
</div>
|
||||
<!-- Wrong number -->
|
||||
<div class="layui-inline">
|
||||
<i class="layui-icon" id="pwr" style="color: red; font-weight: bolder;" hidden>Gauge</i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 确认密码 -->
|
||||
<div class="layui-input-inline">
|
||||
<div class="layui-inline" style="width: 85%">
|
||||
<input type="password" id="rpwd" name="repassword" required lay-verify="required" placeholder="{{ "请确认密码" | translate }}" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<!-- 对号 -->
|
||||
<div class="layui-inline">
|
||||
<i class="layui-icon" id="rpri" style="color: green;font-weight: bolder;" hidden></i>
|
||||
</div>
|
||||
<!-- 错号 -->
|
||||
<div class="layui-inline">
|
||||
<i class="layui-icon" id="rpwr" style="color: red; font-weight: bolder;" hidden>ဆ</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="layui-input-inline login-btn" style="width: 85%">
|
||||
<button type="submit" lay-submit lay-filter="sub" class="layui-btn">{{ "注册" | translate }}</button>
|
||||
</div>
|
||||
<hr style="width: 85%" />
|
||||
<p style="width: 85%"><a href="/api/user_action?action=login" class="fl">{{ "已有账号?立即登录" | translate }}</a></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<script src={% static "layui/layui.js" %}></script>
|
||||
<script type="text/javascript">
|
||||
layui.use(['form','jquery','layer'], function () {
|
||||
var form = layui.form;
|
||||
var $ = layui.jquery;
|
||||
var layer = layui.layer;
|
||||
//添加表单失焦事件
|
||||
//验证表单
|
||||
$('#user').blur(function() {
|
||||
var user = $(this).val();
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
// you code ...
|
||||
// 为密码添加正则验证
|
||||
$('#pwd').blur(function() {
|
||||
var reg = /^[\w\S]{8,20}$/;
|
||||
if(!($('#pwd').val().match(reg))){
|
||||
//layer.msg('请输入合法密码');
|
||||
$('#pwr').removeAttr('hidden');
|
||||
$('#pri').attr('hidden','hidden');
|
||||
layer.msg('{{ "请输入8~20位密码。可以包含字母、数字和特殊字符。" | translate }}');
|
||||
}else {
|
||||
$('#pri').removeAttr('hidden');
|
||||
$('#pwr').attr('hidden','hidden');
|
||||
}
|
||||
});
|
||||
|
||||
//验证两次密码是否一致
|
||||
$('#rpwd').blur(function() {
|
||||
if($('#pwd').val() != $('#rpwd').val()){
|
||||
$('#rpwr').removeAttr('hidden');
|
||||
$('#rpri').attr('hidden','hidden');
|
||||
layer.msg('{{ "两次输入密码不一致!" | translate }}');
|
||||
}else {
|
||||
$('#rpri').removeAttr('hidden');
|
||||
$('#rpwr').attr('hidden','hidden');
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
//添加表单监听事件,提交注册信息
|
||||
form.on('submit(sub)', function() {
|
||||
$.ajax({
|
||||
url:'/api/user_action?action=register',
|
||||
type:'post',
|
||||
dataType:'json',
|
||||
data:{
|
||||
user:$('#user').val(),
|
||||
pwd:$('#pwd').val(),
|
||||
},
|
||||
success:function(data){
|
||||
if (data.code == 1) {
|
||||
layer.msg('{{ "注册成功,请前往登录页登录。" | translate }}');
|
||||
///location.href = "login.html";
|
||||
}else {
|
||||
layer.msg(data.msg);
|
||||
}
|
||||
}
|
||||
})
|
||||
//防止页面跳转
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
106
api/templates/share.html
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
{% extends "base.html" %}{% load static %}
|
||||
{% load my_filters %}
|
||||
{% block title %}{{ "分享机器" | translate }}{% endblock %}
|
||||
{% block link %}<link rel="stylesheet" href="{% static 'layui/css/style.css' %}">{% endblock %}
|
||||
{% block legend_name %}{{ "分享机器给其他用户" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="layui-container">
|
||||
<div class="layui-card layui-col-md3-offset2">
|
||||
<div class="layui-card-header">{{ "请将要分享的机器调整到右侧" | translate }}</div>
|
||||
<div id="showdevice"></div>
|
||||
<button id="create" type="button" class="layui-btn padding-5" lay-on="getData">{{ "生成分享链接" | translate }}</button>
|
||||
</div>
|
||||
<div class="layui-card">{{ "1、链接有效期为15分钟,切勿随意分享给他人。" | translate }}</div>
|
||||
<div class="layui-card">{{ "2、所分享的机器,被分享人享有相同的权限,如果机器设置了保存密码,被分享人也可以直接连接。" | translate }}</div>
|
||||
<div class="layui-card">{{ "3、为保障安全,链接有效期为15分钟、链接仅有效1次。链接一旦被(非分享人的登录用户)访问,分享生效,后续访问链接失效。" | translate }}</div>
|
||||
|
||||
<div class="layui-card layui-col-md6-offset1">
|
||||
<table class="layui-table">
|
||||
<colgroup>
|
||||
<col width="30">
|
||||
<col width="150">
|
||||
<col width="200">
|
||||
<col>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ "链接地址" | translate }}</th>
|
||||
<th>{{ "创建时间" | translate }}</th>
|
||||
<th>{{ "ID列表" | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for one in sharelinks %}
|
||||
<tr>
|
||||
<td><script> document.write(window.location);</script>/{{one.shash}} </td>
|
||||
<td>{{one.create_time}} </td>
|
||||
<td>{{one.peers}} </td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['transfer', 'jquery', 'layer'], function(){
|
||||
var transfer = layui.transfer;
|
||||
var $ = layui.jquery;
|
||||
var layer = layui.layer;
|
||||
|
||||
//渲染
|
||||
transfer.render({
|
||||
elem: '#showdevice' //绑定元素
|
||||
,title: ['{{ "我的机器" | translate }}', '{{ "分享机器" | translate }}'] //自定义标题
|
||||
//,width: 500 //定义宽度
|
||||
//,height: 300 //定义高度
|
||||
,data: [//定义数据源
|
||||
{%for peer in peers %}
|
||||
{"value": "{{peer.id}}", "title": "{{peer.name}}"},
|
||||
{%endfor%}
|
||||
|
||||
] //disabled Whether to disable checked Whether to choose
|
||||
,id: 'device' //Define indexes You can use it when reloading RELOAD or getting the right data
|
||||
});
|
||||
$("#create_bak").click(function(){
|
||||
|
||||
var getData = transfer.getData('device');
|
||||
alert(JSON.stringify(getData));
|
||||
|
||||
});
|
||||
$("#create").click(function(){
|
||||
var getData = transfer.getData('device');
|
||||
$.ajax({
|
||||
url:'/api/share',
|
||||
type:'post',
|
||||
dataType:'json',
|
||||
data:{
|
||||
data:JSON.stringify(getData),
|
||||
},
|
||||
success:function(data){
|
||||
if (data.code == 1) {
|
||||
// var myMsg = layer.msg('处理中', {
|
||||
// shade: 0.4,
|
||||
// time:false //取消自动关闭
|
||||
// });
|
||||
//layer.msg('注册成功,请前往登录页登录。');
|
||||
layer.alert('{{ "成功!如需分享,请复制以下链接给其他人:<br>" | translate }}'+ window.location + '/' +data.shash, function (index) {
|
||||
location.reload();});
|
||||
}else {
|
||||
layer.msg(data.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
66
api/templates/show_conn_log.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "Connection Log" | translate }}{% endblock %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<script src="{% static 'js/sorttable.js' %}"></script>
|
||||
<div style="padding: 20px; background-color: #F2F2F2;">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md15">
|
||||
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{{ "Connection Log" }}:【{{u.username}}】</div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-table sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User IP</th>
|
||||
<th>User ID</th>
|
||||
<th>User Alias</th>
|
||||
<th>Remote ID</th>
|
||||
<th>Remote Alias</th>
|
||||
<th>Connection Start Time</th>
|
||||
<th>Connection End Time</th>
|
||||
<th>Duration (HH:MM:SS)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for one in page_obj %}
|
||||
<tr>
|
||||
<td>{{one.from_ip}}</td>
|
||||
<td>{{one.from_id}}</td>
|
||||
<td>{{one.from_alias}}</td>
|
||||
<td>{{one.rid}}</td>
|
||||
<td>{{one.alias}}</td>
|
||||
<td>{{one.conn_start}}</td>
|
||||
<td>{{one.conn_end}}</td>
|
||||
<td>{{one.duration}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md4 layui-col-md-offset4">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<button class="layui-btn" ><a href="?page=1">« {{ "首页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
|
||||
{% endif %}
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<span class="current">
|
||||
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<button class="layui-btn" > <a href="?page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} »</a></button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
72
api/templates/show_file_log.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "File Transfer Log" | translate }}{% endblock %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<script src="{% static 'js/sorttable.js' %}"></script>
|
||||
<div style="padding: 20px; background-color: #F2F2F2;">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md15">
|
||||
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{{ "File Transfer Log" }}:【{{u.username}}】</div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-table sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Remote ID</th>
|
||||
<th>Remote Alias</th>
|
||||
<th>User ID</th>
|
||||
<th>User Alias</th>
|
||||
<th>User IP</th>
|
||||
<th>Filesize</th>
|
||||
<th>Sent/Received</th>
|
||||
<th>Logged At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for one in page_obj %}
|
||||
<tr>
|
||||
<td>{{one.file}}</td>
|
||||
<td>{{one.remote_id}} </td>
|
||||
<td>{{one.remote_alias}}</td>
|
||||
<td>{{one.user_id}}</td>
|
||||
<td>{{one.user_alias}}</td>
|
||||
<td>{{one.user_ip}}</td>
|
||||
<td>{{one.filesize}}</td>
|
||||
{% if one.direction == 0 %}
|
||||
<td>User Received File</td>
|
||||
{% else %}
|
||||
<td>User Sent File</td>
|
||||
{% endif %}
|
||||
<td>{{one.logged_at}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md4 layui-col-md-offset4">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<button class="layui-btn" ><a href="?page=1">« {{ "首页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
|
||||
{% endif %}
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<span class="current">
|
||||
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<button class="layui-btn" > <a href="?page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} »</a></button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
14
api/templates/show_sys_info.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "Server Information" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
<br>Hostname:
|
||||
{{ hostname }}
|
||||
<br>CPU Usage:
|
||||
{{ cpu_usage }}
|
||||
<br>Memory Usage:
|
||||
{{ memory_usage }}
|
||||
<br>Disk Usage:
|
||||
{{ disk_usage }}
|
||||
{% endblock %}
|
161
api/templates/show_work.html
Normal file
@@ -0,0 +1,161 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "Devices" | translate }}{% endblock %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<script src="{% static 'js/sorttable.js' %}"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<div style="padding: 20px; background-color: #F2F2F2;">
|
||||
<div class="layui-row layui-col-space15">
|
||||
{% if not show_all %}
|
||||
<div class="layui-col-md15">
|
||||
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{{ online_count_single }}/{{page_obj.paginator.count }}: {{ "User Devices Online" | translate }} - 【{{ "用户名" | translate }}:{{u.username}}】
|
||||
<a href="/api/add_peer"><i class='fa fa-plus'></i> Add Client</a>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-table sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Edit / Delete</th>
|
||||
<th>{{ "客户端ID" | translate }}</th>
|
||||
<th>{{ "状态" | translate }}</th>
|
||||
<th>{{ "别名" | translate }}</th>
|
||||
<th>{{ "版本" | translate }}</th>
|
||||
<th>{{ "连接密码" | translate }}</th>
|
||||
<th>{{ "系统用户名" | translate }}</th>
|
||||
<th>{{ "计算机名" | translate }}</th>
|
||||
<th>{{ "平台" | translate }}</th>
|
||||
<th>{{ "系统" | translate }}</th>
|
||||
<th>{{ "CPU" | translate }}</th>
|
||||
<th>{{ "内存" | translate }}</th>
|
||||
<th>{{ "注册时间" | translate }}</th>
|
||||
<th>{{ "更新时间" | translate }}</th>
|
||||
<th>{{ "IP Address" | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for one in page_obj %}
|
||||
<tr>
|
||||
<td><a href="/api/edit_peer?rid={{one.rid}}"><i class='fa fa-edit'></i></a> / <a href="/api/delete_peer?rid={{one.rid}}"><i class='fa fa-trash'></i></a></td>
|
||||
<td><a href=rustdesk://{{one.rid}}>{{one.rid}}</a> </td>
|
||||
<td>{{one.status}} </td>
|
||||
<td>{{one.alias}}</td>
|
||||
<td>{{one.version}}</td>
|
||||
<td>{{one.has_rhash}}</td>
|
||||
<td>{{one.username}}</td>
|
||||
<td>{{one.hostname}}</td>
|
||||
<td>{{one.platform}}</td>
|
||||
<td>{{one.os}}</td>
|
||||
<td>{{one.cpu}}</td>
|
||||
<td>{{one.memory}}</td>
|
||||
<td>{{one.create_time}}</td>
|
||||
<td>{{one.update_time}}</td>
|
||||
<td>{{one.ip}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md4 layui-col-md-offset4">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<button class="layui-btn" ><a href="?page=1">« {{ "首页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
|
||||
{% endif %}
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<span class="current">
|
||||
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<button class="layui-btn" > <a href="?page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} »</a></button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if u.is_admin and show_all %}
|
||||
<div class="layui-col-md15">
|
||||
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{{ online_count_all }}/{{page_obj.paginator.count }}: {{ "All Devices" | translate }} »
|
||||
<div class="layui-btn" ><a href="/api/down_peers">{{ "导出xlsx" | translate }}</a></div>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-table sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ "客户端ID" | translate }}</th>
|
||||
<th>{{ "所属用户" | translate }}</th>
|
||||
<th>{{ "版本" | translate }}</th>
|
||||
<th>{{ "系统用户名" | translate }}</th>
|
||||
<th>{{ "计算机名" | translate }}</th>
|
||||
<th>{{ "系统" | translate }}</th>
|
||||
<th>{{ "CPU" | translate }}</th>
|
||||
<th>{{ "内存" | translate }}</th>
|
||||
<th>{{ "注册日期" | translate }}</th>
|
||||
<th>{{ "更新时间" | translate }}</th>
|
||||
<th>{{ "状态" | translate }}</th>
|
||||
<th>{{ "IP Address" | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for one in page_obj %}
|
||||
<tr>
|
||||
<td><a href=rustdesk://{{one.rid}}>{{one.rid}}</a> </td>
|
||||
{% if one.rust_user|length > 0 %}
|
||||
<td>{{one.rust_user}} </td>
|
||||
{% else %}
|
||||
<td><a href="/api/assign_peer?rid={{one.rid}}"><i class='fa fa-plus'></i> Assign Peer</a></td>
|
||||
{% endif %}
|
||||
<td>{{one.version}} </td>
|
||||
<td>{{one.username}} </td>
|
||||
<td>{{one.hostname}} </td>
|
||||
<td>{{one.os}} </td>
|
||||
<td>{{one.cpu}} </td>
|
||||
<td>{{one.memory}} </td>
|
||||
<td>{{one.create_time}} </td>
|
||||
<td>{{one.update_time}} </td>
|
||||
<td>{{one.status}} </td>
|
||||
<td>{{one.ip}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md4 layui-col-md-offset4">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<button class="layui-btn" ><a href="?show_type=admin&page=1">« {{ "首页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?show_type=admin&page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
|
||||
{% endif %}
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<span class="current">
|
||||
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<button class="layui-btn" > <a href="?show_type=admin&page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
|
||||
<button class="layui-btn" ><a href="?show_type=admin&page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} »</a></button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
13
api/templates/waiting.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends phone_or_desktop %}
|
||||
{% load my_filters %}
|
||||
{% block title %}RustDesk WebUI{% endblock %}
|
||||
{% block legend_name %}{{ "Client Generator" | translate }}{% endblock %}
|
||||
{% block content %}
|
||||
Please wait...This can take 20-30 minutes (or longer if there are other users).<br><br>
|
||||
Status: {{status}}
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
window.location.replace('/api/check_for_file?filename={{filename}}&uuid={{uuid}}&platform={{platform}}');
|
||||
}, 5000); // 5000 milliseconds = 5 seconds
|
||||
</script>
|
||||
{% endblock %}
|
0
api/templatetags/__init__.py
Normal file
8
api/templatetags/my_filters.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django import template
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def translate(text):
|
||||
return _(text)
|
3
api/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
42
api/urls.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import django
|
||||
if django.__version__.split('.')[0]>='4':
|
||||
from django.urls import re_path as url
|
||||
else:
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from api import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^login',views.login),
|
||||
url(r'^logout',views.logout),
|
||||
url(r'^ab$',views.ab),
|
||||
url(r'^ab\/get',views.ab_get), # 兼容 x86-sciter 版客户端
|
||||
url(r'^users',views.users),
|
||||
url(r'^peers',views.peers),
|
||||
url(r'^currentUser',views.currentUser),
|
||||
url(r'^sysinfo',views.sysinfo),
|
||||
url(r'^heartbeat',views.heartbeat),
|
||||
#url(r'^register',views.register),
|
||||
url(r'^user_action',views.user_action), # 前端
|
||||
url(r'^work',views.work), # 前端
|
||||
url(r'^down_peers$',views.down_peers), # 前端
|
||||
url(r'^share',views.share), # 前端
|
||||
url(r'^conn_log',views.conn_log),
|
||||
url(r'^file_log',views.file_log),
|
||||
url(r'^audit',views.audit),
|
||||
url(r'^sys_info',views.sys_info),
|
||||
url(r'^clients',views.clients),
|
||||
url(r'^download',views.download),
|
||||
url(r'^generator',views.generator_view),
|
||||
url(r'^check_for_file',views.check_for_file),
|
||||
url(r'^download_client',views.download_client),
|
||||
url(r'^creategh',views.create_github_run),
|
||||
url(r'^updategh',views.update_github_run),
|
||||
url(r'^save_custom_client',views.save_custom_client),
|
||||
url(r'^delete_file',views.delete_file),
|
||||
url(r'^get_png',views.get_png),
|
||||
url(r'^add_peer',views.add_peer),
|
||||
url(r'^delete_peer',views.delete_peer),
|
||||
url(r'^edit_peer',views.edit_peer),
|
||||
url(r'^assign_peer',views.assign_peer),
|
||||
]
|
36
api/util.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Thu Nov 19 15:51:21 2020
|
||||
|
||||
@author: lenovo
|
||||
"""
|
||||
|
||||
import platform
|
||||
import logging
|
||||
from .models_user import UserProfile
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from django.conf import settings as _settings
|
||||
|
||||
def settings(request):
|
||||
"""
|
||||
TEMPLATE_CONTEXT_PROCESSORS
|
||||
"""
|
||||
context = { 'settings': _settings }
|
||||
try:
|
||||
username = request.user
|
||||
u = UserProfile.objects.get(username=username)
|
||||
context['test'] = 'This is a test variable'
|
||||
context['u'] = u
|
||||
#context['user'] = u
|
||||
context['username'] = username
|
||||
context['is_admin'] = u.is_admin
|
||||
context['is_active'] = u.is_active
|
||||
context['domain'] = _settings.ID_SERVER
|
||||
context['is_windows'] = True if platform.system() == 'Windows' else False
|
||||
|
||||
|
||||
logger.info("set system status variable")
|
||||
except Exception as e:
|
||||
logger.error("settings:{}".format( e))
|
||||
return context
|
26
api/views.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import auth
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from itertools import chain
|
||||
from django.db.models.fields import DateTimeField, DateField, CharField, TextField
|
||||
|
||||
from django.db.models import Model
|
||||
|
||||
from django.http import JsonResponse
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
import hashlib
|
||||
from api.models import RustDeskToken, UserProfile, RustDeskTag, RustDeskPeer, RustDesDevice
|
||||
|
||||
import copy
|
||||
|
||||
from .views_front import *
|
||||
from .views_api import *
|
||||
from .views_generator import *
|
||||
from .front_locale import *
|
329
api/views_api.py
Normal file
@@ -0,0 +1,329 @@
|
||||
# cython:language_level=3
|
||||
from django.http import JsonResponse
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
import hashlib
|
||||
import math
|
||||
from django.contrib import auth
|
||||
from django.forms.models import model_to_dict
|
||||
from api.models import RustDeskToken, UserProfile, RustDeskTag, RustDeskPeer, RustDesDevice, ConnLog, FileLog
|
||||
from django.db.models import Q
|
||||
import copy
|
||||
from .views_front import *
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
def login(request):
|
||||
result = {}
|
||||
if request.method == 'GET':
|
||||
result['error'] = _('请求方式错误!请使用POST方式。')
|
||||
return JsonResponse(result)
|
||||
|
||||
data = json.loads(request.body.decode())
|
||||
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
rid = data.get('id', '')
|
||||
uuid = data.get('uuid', '')
|
||||
autoLogin = data.get('autoLogin', True)
|
||||
rtype = data.get('type', '')
|
||||
deviceInfo = data.get('deviceInfo', '')
|
||||
user = auth.authenticate(username=username,password=password)
|
||||
if not user:
|
||||
result['error'] = _('帐号或密码错误!请重试,多次重试后将被锁定IP!')
|
||||
return JsonResponse(result)
|
||||
user.rid = rid
|
||||
user.uuid = uuid
|
||||
user.autoLogin = autoLogin
|
||||
user.rtype = rtype
|
||||
user.deviceInfo = json.dumps(deviceInfo)
|
||||
user.save()
|
||||
|
||||
token = RustDeskToken.objects.filter(Q(uid=user.id) & Q(username=user.username) & Q(rid=user.rid)).first()
|
||||
|
||||
# Check whether
|
||||
if token:
|
||||
now_t = datetime.datetime.now()
|
||||
nums = (now_t - token.create_time).seconds if now_t > token.create_time else 0
|
||||
if nums >= EFFECTIVE_SECONDS:
|
||||
token.delete()
|
||||
token = None
|
||||
|
||||
if not token:
|
||||
# Get and save token
|
||||
token = RustDeskToken(
|
||||
username=user.username,
|
||||
uid=user.id,
|
||||
uuid=user.uuid,
|
||||
rid=user.rid,
|
||||
access_token=getStrMd5(str(time.time())+salt)
|
||||
)
|
||||
token.save()
|
||||
|
||||
result['access_token'] = token.access_token
|
||||
result['type'] = 'access_token'
|
||||
result['user'] = {'name':user.username}
|
||||
return JsonResponse(result)
|
||||
|
||||
|
||||
def logout(request):
|
||||
if request.method == 'GET':
|
||||
result = {'error':_('请求方式错误!')}
|
||||
return JsonResponse(result)
|
||||
|
||||
data = json.loads(request.body.decode())
|
||||
rid = data.get('id', '')
|
||||
uuid = data.get('uuid', '')
|
||||
user = UserProfile.objects.filter(Q(rid=rid) & Q(uuid=uuid)).first()
|
||||
if not user:
|
||||
result = {'error':_('异常请求!')}
|
||||
return JsonResponse(result)
|
||||
token = RustDeskToken.objects.filter(Q(uid=user.id) & Q(rid=user.rid)).first()
|
||||
if token:
|
||||
token.delete()
|
||||
|
||||
result = {'code':1}
|
||||
return JsonResponse(result)
|
||||
|
||||
|
||||
def currentUser(request):
|
||||
result = {}
|
||||
if request.method == 'GET':
|
||||
result['error'] = _('错误的提交方式!')
|
||||
return JsonResponse(result)
|
||||
postdata = json.loads(request.body)
|
||||
rid = postdata.get('id', '')
|
||||
uuid = postdata.get('uuid', '')
|
||||
|
||||
access_token = request.META.get('HTTP_AUTHORIZATION', '')
|
||||
access_token = access_token.split('Bearer ')[-1]
|
||||
token = RustDeskToken.objects.filter(Q(access_token=access_token) ).first()
|
||||
user = None
|
||||
if token:
|
||||
user = UserProfile.objects.filter(Q(id=token.uid)).first()
|
||||
|
||||
if user:
|
||||
if token:
|
||||
result['access_token'] = token.access_token
|
||||
result['type'] = 'access_token'
|
||||
result['name'] = user.username
|
||||
return JsonResponse(result)
|
||||
|
||||
|
||||
def ab(request):
|
||||
'''
|
||||
'''
|
||||
access_token = request.META.get('HTTP_AUTHORIZATION', '')
|
||||
access_token = access_token.split('Bearer ')[-1]
|
||||
token = RustDeskToken.objects.filter(Q(access_token=access_token) ).first()
|
||||
if not token:
|
||||
result = {'error':_('拉取列表错误!')}
|
||||
return JsonResponse(result)
|
||||
|
||||
if request.method == 'GET':
|
||||
result = {}
|
||||
uid = token.uid
|
||||
tags = RustDeskTag.objects.filter(Q(uid=uid) )
|
||||
tag_names = []
|
||||
tag_colors = {}
|
||||
if tags:
|
||||
tag_names = [str(x.tag_name) for x in tags]
|
||||
tag_colors = {str(x.tag_name):int(x.tag_color) for x in tags if x.tag_color!=''}
|
||||
|
||||
peers_result = []
|
||||
peers = RustDeskPeer.objects.filter(Q(uid=uid) )
|
||||
if peers:
|
||||
for peer in peers:
|
||||
tmp = {
|
||||
'id':peer.rid,
|
||||
'username':peer.username,
|
||||
'hostname':peer.hostname,
|
||||
'alias':peer.alias,
|
||||
'platform':peer.platform,
|
||||
'tags':peer.tags.split(','),
|
||||
'hash':peer.rhash,
|
||||
}
|
||||
peers_result.append(tmp)
|
||||
|
||||
result['updated_at'] = datetime.datetime.now()
|
||||
result['data'] = {
|
||||
'tags':tag_names,
|
||||
'peers':peers_result,
|
||||
'tag_colors':json.dumps(tag_colors)
|
||||
}
|
||||
result['data'] = json.dumps(result['data'])
|
||||
return JsonResponse(result)
|
||||
else:
|
||||
postdata = json.loads(request.body.decode())
|
||||
data = postdata.get('data', '')
|
||||
data = {} if data=='' else json.loads(data)
|
||||
tagnames = data.get('tags', [])
|
||||
tag_colors = data.get('tag_colors', '')
|
||||
tag_colors = {} if tag_colors=='' else json.loads(tag_colors)
|
||||
peers = data.get('peers', [])
|
||||
|
||||
if tagnames:
|
||||
# Delete the old tag
|
||||
RustDeskTag.objects.filter(uid=token.uid).delete()
|
||||
# Increase
|
||||
newlist = []
|
||||
for name in tagnames:
|
||||
tag = RustDeskTag(
|
||||
uid=token.uid,
|
||||
tag_name=name,
|
||||
tag_color=tag_colors.get(name, '')
|
||||
)
|
||||
newlist.append(tag)
|
||||
RustDeskTag.objects.bulk_create(newlist)
|
||||
if peers:
|
||||
RustDeskPeer.objects.filter(uid=token.uid).delete()
|
||||
newlist = []
|
||||
for one in peers:
|
||||
peer = RustDeskPeer(
|
||||
uid=token.uid,
|
||||
rid=one['id'],
|
||||
username=one['username'],
|
||||
hostname=one['hostname'],
|
||||
alias=one['alias'],
|
||||
platform=one['platform'],
|
||||
tags=','.join(one['tags']),
|
||||
rhash=one['hash'],
|
||||
|
||||
|
||||
)
|
||||
newlist.append(peer)
|
||||
RustDeskPeer.objects.bulk_create(newlist)
|
||||
|
||||
result = {
|
||||
'code':102,
|
||||
'data':_('更新地址簿有误')
|
||||
}
|
||||
return JsonResponse(result)
|
||||
|
||||
def ab_get(request):
|
||||
# 兼容 x86-sciter 版客户端,此版客户端通过访问 "POST /api/ab/get" 来获取地址簿
|
||||
request.method = 'GET'
|
||||
return ab(request)
|
||||
|
||||
def sysinfo(request):
|
||||
# 客户端注册服务后,才会发送设备信息
|
||||
result = {}
|
||||
if request.method == 'GET':
|
||||
result['error'] = _('错误的提交方式!')
|
||||
return JsonResponse(result)
|
||||
|
||||
client_ip = get_client_ip(request)
|
||||
postdata = json.loads(request.body)
|
||||
device = RustDesDevice.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).first()
|
||||
if not device:
|
||||
device = RustDesDevice(
|
||||
rid=postdata['id'],
|
||||
cpu=postdata['cpu'],
|
||||
hostname=postdata['hostname'],
|
||||
memory=postdata['memory'],
|
||||
os=postdata['os'],
|
||||
username=postdata.get('username', '-'),
|
||||
uuid=postdata['uuid'],
|
||||
version=postdata['version'],
|
||||
ip=client_ip,
|
||||
)
|
||||
device.save()
|
||||
else:
|
||||
postdata2 = copy.copy(postdata)
|
||||
postdata2['rid'] = postdata2['id']
|
||||
postdata2.pop('id')
|
||||
postdata2['ip'] = client_ip
|
||||
RustDesDevice.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).update(**postdata2)
|
||||
result['data'] = 'ok'
|
||||
return JsonResponse(result)
|
||||
|
||||
def heartbeat(request):
|
||||
postdata = json.loads(request.body)
|
||||
device = RustDesDevice.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).first()
|
||||
if device:
|
||||
device.save()
|
||||
# token保活
|
||||
create_time = datetime.datetime.now() + datetime.timedelta(seconds=EFFECTIVE_SECONDS)
|
||||
RustDeskToken.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).update(create_time=create_time)
|
||||
result = {}
|
||||
result['data'] = _('在线')
|
||||
return JsonResponse(result)
|
||||
|
||||
def get_client_ip(request):
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
def convert_filesize(size_bytes):
|
||||
if size_bytes == 0:
|
||||
return "0B"
|
||||
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
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])
|
||||
|
||||
|
||||
def audit(request):
|
||||
postdata = json.loads(request.body)
|
||||
#print(postdata)
|
||||
audit_type = postdata['action'] if 'action' in postdata else ''
|
||||
if audit_type == 'new':
|
||||
new_conn_log = ConnLog(
|
||||
action=postdata['action'] if 'action' in postdata else '',
|
||||
conn_id=postdata['conn_id'] if 'conn_id' in postdata else 0,
|
||||
from_ip=postdata['ip'] if 'ip' in postdata else '',
|
||||
from_id='',
|
||||
rid=postdata['id'] if 'id' in postdata else '',
|
||||
conn_start=datetime.datetime.now(),
|
||||
session_id=postdata['session_id'] if 'session_id' in postdata else 0,
|
||||
uuid=postdata['uuid'] if 'uuid' in postdata else '',
|
||||
)
|
||||
new_conn_log.save()
|
||||
elif audit_type =="close":
|
||||
ConnLog.objects.filter(Q(conn_id=postdata['conn_id'])).update(conn_end=datetime.datetime.now())
|
||||
elif 'is_file' in postdata:
|
||||
print(postdata)
|
||||
files = json.loads(postdata['info'])['files']
|
||||
filesize = convert_filesize(int(files[0][1]))
|
||||
new_file_log = FileLog(
|
||||
file=postdata['path'],
|
||||
user_id=postdata['peer_id'],
|
||||
user_ip=json.loads(postdata['info'])['ip'],
|
||||
remote_id=postdata['id'],
|
||||
filesize=filesize,
|
||||
direction=postdata['type'],
|
||||
logged_at=datetime.datetime.now(),
|
||||
)
|
||||
new_file_log.save()
|
||||
else:
|
||||
try:
|
||||
peer = postdata['peer']
|
||||
ConnLog.objects.filter(Q(conn_id=postdata['conn_id'])).update(session_id=postdata['session_id'])
|
||||
ConnLog.objects.filter(Q(conn_id=postdata['conn_id'])).update(from_id=peer[0])
|
||||
except:
|
||||
print(postdata)
|
||||
|
||||
result = {
|
||||
'code':1,
|
||||
'data':'ok'
|
||||
}
|
||||
return JsonResponse(result)
|
||||
|
||||
def users(request):
|
||||
result = {
|
||||
'code':1,
|
||||
'data':_('好的')
|
||||
}
|
||||
return JsonResponse(result)
|
||||
|
||||
def peers(request):
|
||||
result = {
|
||||
'code':1,
|
||||
'data':'ok'
|
||||
}
|
||||
return JsonResponse(result)
|
719
api/views_front.py
Normal file
@@ -0,0 +1,719 @@
|
||||
# cython:language_level=3
|
||||
from pathlib import Path
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import auth
|
||||
from api.models import RustDeskPeer, RustDesDevice, UserProfile, ShareLink, ConnLog, FileLog
|
||||
from django.forms.models import model_to_dict
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
|
||||
from itertools import chain
|
||||
from django.db.models.fields import DateTimeField, DateField, CharField, TextField
|
||||
import datetime
|
||||
from django.db.models import Model
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
import sys
|
||||
from dateutil import tz
|
||||
import platform
|
||||
import psutil
|
||||
import os
|
||||
|
||||
from io import BytesIO
|
||||
import xlwt
|
||||
from django.utils.translation import gettext as _
|
||||
from .forms import AddPeerForm, EditPeerForm, AssignPeerForm
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
salt = 'xiaomo'
|
||||
EFFECTIVE_SECONDS = 7200
|
||||
|
||||
def getStrMd5(s):
|
||||
if not isinstance(s, (str,)):
|
||||
s = str(s)
|
||||
|
||||
myHash = hashlib.md5()
|
||||
myHash.update(s.encode())
|
||||
|
||||
return myHash.hexdigest()
|
||||
|
||||
def model_to_dict2(instance, fields=None, exclude=None, replace=None, default=None):
|
||||
"""
|
||||
:params instance: Model object, not the QuerySet data set
|
||||
:params fields: Specify the field data to be displayed,('Field 1','Field 2')
|
||||
:params exclude: Specify the field data that is eliminated,('Field 1','Field 2')
|
||||
:params replace: Modify the field name to the required name,{'Database field name':'Front -end display name'}
|
||||
:params default: Added no existing field data,{'Field':'data'}
|
||||
"""
|
||||
# 对传递进来的模型对象校验
|
||||
if not isinstance(instance, Model):
|
||||
raise Exception(_('model_to_dict接收的参数必须是模型对象'))
|
||||
# 对替换数据库字段名字校验
|
||||
if replace and type(replace) == dict:
|
||||
for replace_field in replace.values():
|
||||
if hasattr(instance, replace_field):
|
||||
raise Exception(_(f'model_to_dict,要替换成{replace_field}字段已经存在了'))
|
||||
# 对要新增的默认值进行校验
|
||||
if default and type(default) == dict:
|
||||
for default_key in default.keys():
|
||||
if hasattr(instance, default_key):
|
||||
raise Exception(_(f'model_to_dict,要新增默认值,但字段{default_key}已经存在了'))
|
||||
opts = instance._meta
|
||||
data = {}
|
||||
for f in chain(opts.concrete_fields, opts.private_fields, opts.many_to_many):
|
||||
# 源码下:这块代码会将时间字段剔除掉,我加上一层判断,让其不再剔除时间字段
|
||||
if not getattr(f, 'editable', False):
|
||||
if type(f) == DateField or type(f) == DateTimeField:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
# 如果fields参数传递了,要进行判断
|
||||
if fields is not None and f.name not in fields:
|
||||
continue
|
||||
# 如果exclude 传递了,要进行判断
|
||||
if exclude and f.name in exclude:
|
||||
continue
|
||||
|
||||
key = f.name
|
||||
# 获取字段对应的数据
|
||||
if type(f) == DateTimeField:
|
||||
# 字段类型是,DateTimeFiled 使用自己的方式操作
|
||||
value = getattr(instance, key)
|
||||
value = datetime.datetime.strftime(value, '%Y-%m-%d %H:%M')
|
||||
elif type(f) == DateField:
|
||||
# 字段类型是,DateFiled 使用自己的方式操作
|
||||
value = getattr(instance, key)
|
||||
value = datetime.datetime.strftime(value, '%Y-%m-%d')
|
||||
elif type(f) == CharField or type(f) == TextField:
|
||||
# 字符串数据是否可以进行序列化,转成python结构数据
|
||||
value = getattr(instance, key)
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except Exception as _:
|
||||
value = value
|
||||
else:#其他类型的字段
|
||||
# value = getattr(instance, key)
|
||||
key = f.name
|
||||
value = f.value_from_object(instance)
|
||||
# data[f.name] = f.value_from_object(instance)
|
||||
# 1、替换字段名字
|
||||
if replace and key in replace.keys():
|
||||
key = replace.get(key)
|
||||
data[key] = value
|
||||
#2、新增默认的字段数据
|
||||
if default:
|
||||
data.update(default)
|
||||
return data
|
||||
|
||||
|
||||
def index(request):
|
||||
print('sdf',sys.argv)
|
||||
if request.user and request.user.username!='AnonymousUser':
|
||||
return HttpResponseRedirect('/api/work')
|
||||
return HttpResponseRedirect('/api/user_action?action=login')
|
||||
|
||||
|
||||
def user_action(request):
|
||||
action = request.GET.get('action', '')
|
||||
if action == 'login':
|
||||
return user_login(request)
|
||||
elif action == 'register':
|
||||
return user_register(request)
|
||||
elif action == 'logout':
|
||||
return user_logout(request)
|
||||
else:
|
||||
return
|
||||
|
||||
def user_login(request):
|
||||
if request.method == 'GET':
|
||||
return render(request, 'login.html')
|
||||
|
||||
username = request.POST.get('account', '')
|
||||
password = request.POST.get('password', '')
|
||||
if not username or not password:
|
||||
return JsonResponse({'code':0, 'msg':_('出了点问题,未获取用户名或密码。')})
|
||||
|
||||
user = auth.authenticate(username=username,password=password)
|
||||
if user:
|
||||
auth.login(request, user)
|
||||
return JsonResponse({'code':1, 'url':'/api/work'})
|
||||
else:
|
||||
return JsonResponse({'code':0, 'msg':_('帐号或密码错误!')})
|
||||
|
||||
def user_register(request):
|
||||
info = ''
|
||||
if request.method == 'GET':
|
||||
return render(request, 'reg.html')
|
||||
ALLOW_REGISTRATION = settings.ALLOW_REGISTRATION
|
||||
result = {
|
||||
'code':0,
|
||||
'msg':''
|
||||
}
|
||||
if not ALLOW_REGISTRATION:
|
||||
result['msg'] = _('当前未开放注册,请联系管理员!')
|
||||
return JsonResponse(result)
|
||||
|
||||
username = request.POST.get('user', '')
|
||||
password1 = request.POST.get('pwd', '')
|
||||
|
||||
if len(username) <= 3:
|
||||
info = _('用户名不得小于3位')
|
||||
result['msg'] = info
|
||||
return JsonResponse(result)
|
||||
|
||||
if len(password1)<8 or len(password1)>20:
|
||||
info = _('密码长度不符合要求, 应在8~20位。')
|
||||
result['msg'] = info
|
||||
return JsonResponse(result)
|
||||
|
||||
user = UserProfile.objects.filter(Q(username=username)).first()
|
||||
if user:
|
||||
info = _('用户名已存在。')
|
||||
result['msg'] = info
|
||||
return JsonResponse(result)
|
||||
user = UserProfile(
|
||||
username=username,
|
||||
password=make_password(password1),
|
||||
is_admin = True if UserProfile.objects.count()==0 else False,
|
||||
is_superuser = True if UserProfile.objects.count()==0 else False,
|
||||
is_active = True
|
||||
)
|
||||
user.save()
|
||||
result['msg'] = info
|
||||
result['code'] = 1
|
||||
return JsonResponse(result)
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def user_logout(request):
|
||||
info = ''
|
||||
auth.logout(request)
|
||||
return HttpResponseRedirect('/api/user_action?action=login')
|
||||
|
||||
def get_single_info(uid):
|
||||
online_count = 0
|
||||
peers = RustDeskPeer.objects.filter(Q(uid=uid))
|
||||
rids = [x.rid for x in peers]
|
||||
peers = {x.rid:model_to_dict(x) for x in peers}
|
||||
#print(peers)
|
||||
devices = RustDesDevice.objects.filter(rid__in=rids)
|
||||
devices = {x.rid:x for x in devices}
|
||||
|
||||
for rid in peers.keys():
|
||||
peers[rid]['has_rhash'] = _('yes') if len(peers[rid]['rhash'])>1 else _('no')
|
||||
peers[rid]['status'] = _('X')
|
||||
|
||||
now = datetime.datetime.now()
|
||||
for rid, device in devices.items():
|
||||
peers[rid]['create_time'] = device.create_time.strftime('%Y-%m-%d')
|
||||
peers[rid]['update_time'] = device.update_time.strftime('%Y-%m-%d %H:%M')
|
||||
peers[rid]['version'] = device.version
|
||||
peers[rid]['memory'] = device.memory
|
||||
peers[rid]['cpu'] = device.cpu
|
||||
peers[rid]['os'] = device.os
|
||||
peers[rid]['ip'] = device.ip
|
||||
if (now-device.update_time).seconds <=120:
|
||||
peers[rid]['status'] = _('Online')
|
||||
online_count += 1
|
||||
else:
|
||||
peers[rid]['status'] = _('X')
|
||||
|
||||
sorted_peers = sorted(peers.items(), key=custom_sort, reverse=True)
|
||||
new_ordered_dict = {}
|
||||
for key, peer in sorted_peers:
|
||||
new_ordered_dict[key] = peer
|
||||
|
||||
#return ([v for k,v in peers.items()], online_count)
|
||||
return ([v for k,v in new_ordered_dict.items()], online_count)
|
||||
|
||||
def get_all_info():
|
||||
online_count = 0
|
||||
devices = RustDesDevice.objects.all()
|
||||
peers = RustDeskPeer.objects.all()
|
||||
devices = {x.rid:model_to_dict2(x) for x in devices}
|
||||
now = datetime.datetime.now()
|
||||
for peer in peers:
|
||||
user = UserProfile.objects.filter(Q(id=peer.uid)).first()
|
||||
device = devices.get(peer.rid, None)
|
||||
if device:
|
||||
devices[peer.rid]['rust_user'] = user.username
|
||||
|
||||
for k, v in devices.items():
|
||||
if (now-datetime.datetime.strptime(v['update_time'], '%Y-%m-%d %H:%M')).seconds <=120:
|
||||
devices[k]['status'] = _('Online')
|
||||
online_count += 1
|
||||
else:
|
||||
devices[k]['status'] = _('X')
|
||||
|
||||
sorted_devices = sorted(devices.items(), key=custom_sort, reverse=True)
|
||||
new_ordered_dict = {}
|
||||
for key, device in sorted_devices:
|
||||
new_ordered_dict[key] = device
|
||||
return ([v for k,v in new_ordered_dict.items()], online_count)
|
||||
|
||||
def custom_sort(item):
|
||||
status = item[1]['status']
|
||||
if status == 'Online':
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_conn_log():
|
||||
logs = ConnLog.objects.all()
|
||||
logs = {x.id:model_to_dict(x) for x in logs}
|
||||
|
||||
for k, v in logs.items():
|
||||
try:
|
||||
peer = RustDeskPeer.objects.get(rid=v['rid'])
|
||||
logs[k]['alias'] = peer.alias
|
||||
except:
|
||||
logs[k]['alias'] = 'UNKNOWN'
|
||||
try:
|
||||
peer = RustDeskPeer.objects.get(rid=v['from_id'])
|
||||
logs[k]['from_alias'] = peer.alias
|
||||
except:
|
||||
logs[k]['from_alias'] = 'UNKNOWN'
|
||||
#from_zone = tz.tzutc()
|
||||
#to_zone = tz.tzlocal()
|
||||
#utc = logs[k]['logged_at']
|
||||
#utc = utc.replace(tzinfo=from_zone)
|
||||
#logs[k]['logged_at'] = utc.astimezone(to_zone)
|
||||
try:
|
||||
duration = round((logs[k]['conn_end'] - logs[k]['conn_start']).total_seconds())
|
||||
m, s = divmod(duration, 60)
|
||||
h, m = divmod(m, 60)
|
||||
#d, h = divmod(h, 24)
|
||||
logs[k]['duration'] = f'{h:02d}:{m:02d}:{s:02d}'
|
||||
except:
|
||||
logs[k]['duration'] = -1
|
||||
|
||||
sorted_logs = sorted(logs.items(), key=lambda x: x[1]['conn_start'], reverse=True)
|
||||
new_ordered_dict = {}
|
||||
for key, alog in sorted_logs:
|
||||
new_ordered_dict[key] = alog
|
||||
|
||||
return [v for k, v in new_ordered_dict.items()]
|
||||
|
||||
def get_file_log():
|
||||
logs = FileLog.objects.all()
|
||||
logs = {x.id:model_to_dict(x) for x in logs}
|
||||
|
||||
for k, v in logs.items():
|
||||
try:
|
||||
peer_remote = RustDeskPeer.objects.get(rid=v['remote_id'])
|
||||
logs[k]['remote_alias'] = peer_remote.alias
|
||||
except:
|
||||
logs[k]['remote_alias'] = 'UNKNOWN'
|
||||
try:
|
||||
peer_user = RustDeskPeer.objects.get(rid=v['user_id'])
|
||||
logs[k]['user_alias'] = peer_user.alias
|
||||
except:
|
||||
logs[k]['user_alias'] = 'UNKNOWN'
|
||||
|
||||
sorted_logs = sorted(logs.items(), key=lambda x: x[1]['logged_at'], reverse=True)
|
||||
new_ordered_dict = {}
|
||||
for key, alog in sorted_logs:
|
||||
new_ordered_dict[key] = alog
|
||||
|
||||
return [v for k, v in new_ordered_dict.items()]
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def sys_info(request):
|
||||
hostname = platform.node()
|
||||
cpu_usage = psutil.cpu_percent()
|
||||
memory_usage = psutil.virtual_memory().percent
|
||||
disk_usage = psutil.disk_usage('/').percent
|
||||
print(cpu_usage, memory_usage, disk_usage)
|
||||
return render(request, 'show_sys_info.html', {'hostname':hostname, 'cpu_usage':cpu_usage, 'memory_usage':memory_usage, 'disk_usage':disk_usage, 'phone_or_desktop': is_mobile(request)})
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def clients(request):
|
||||
basedir = os.path.join('clients')
|
||||
androidaarch64 = os.path.join(basedir,'android','aarch64')
|
||||
androidarmv7 = os.path.join(basedir,'android','armv7')
|
||||
linuxaarch64 = os.path.join(basedir,'linux','aarch64')
|
||||
linuxx86_64 = os.path.join(basedir,'linux','x86_64')
|
||||
mocos = os.path.join(basedir,'macOS')
|
||||
sciter = os.path.join(basedir,'sciter')
|
||||
custom = os.path.join(basedir,'custom')
|
||||
client_files = {}
|
||||
client_custom_files = {}
|
||||
if os.path.exists(basedir):
|
||||
for file in os.listdir(basedir):
|
||||
if (file.endswith(".exe") or file.endswith(".msi")):
|
||||
filepath = os.path.join(basedir,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': basedir
|
||||
}
|
||||
if os.path.exists(androidaarch64):
|
||||
for file in os.listdir(androidaarch64):
|
||||
if file.endswith(".apk"):
|
||||
filepath = os.path.join(androidaarch64,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': androidaarch64
|
||||
}
|
||||
if os.path.exists(androidarmv7):
|
||||
for file in os.listdir(androidarmv7):
|
||||
if file.endswith(".apk"):
|
||||
filepath = os.path.join(androidarmv7,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': androidarmv7
|
||||
}
|
||||
if os.path.exists(linuxaarch64):
|
||||
for file in os.listdir(linuxaarch64):
|
||||
if (file.endswith(".rpm") or file.endswith(".deb")):
|
||||
filepath = os.path.join(linuxaarch64,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': linuxaarch64
|
||||
}
|
||||
if os.path.exists(linuxx86_64):
|
||||
for file in os.listdir(linuxx86_64):
|
||||
if (file.endswith(".rpm") or file.endswith(".deb")):
|
||||
filepath = os.path.join(linuxx86_64,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': linuxx86_64
|
||||
}
|
||||
if os.path.exists(mocos):
|
||||
for file in os.listdir(mocos):
|
||||
if file.endswith(".dmg"):
|
||||
filepath = os.path.join(mocos,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': mocos
|
||||
}
|
||||
if os.path.exists(sciter):
|
||||
for file in os.listdir(sciter):
|
||||
if file.endswith(".exe"):
|
||||
filepath = os.path.join(sciter,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': sciter
|
||||
}
|
||||
if os.path.exists(custom):
|
||||
for file in os.listdir(custom):
|
||||
#if file.endswith(".exe"):
|
||||
filepath = os.path.join(custom,file)
|
||||
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
|
||||
client_custom_files[file] = {
|
||||
'file': file,
|
||||
'modified': modified,
|
||||
'path': custom
|
||||
}
|
||||
return render(request, 'clients.html', {'client_files': client_files, 'client_custom_files': client_custom_files, 'phone_or_desktop': is_mobile(request)})
|
||||
|
||||
def download_file(request, filename, path):
|
||||
file_path = os.path.join(str(BASE_DIR),path,filename)
|
||||
with open(file_path, 'rb') as file:
|
||||
response = HttpResponse(file, headers={
|
||||
'Content-Type': 'application/x-binary',
|
||||
'Content-Disposition': f'attachment; filename="{filename}"'
|
||||
})
|
||||
return response
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def download(request):
|
||||
filename = request.GET['filename']
|
||||
path = request.GET['path']
|
||||
return download_file(request, filename, path)
|
||||
|
||||
@login_required(login_url='/api/user_cation?action=login')
|
||||
def delete_file(request):
|
||||
filename = request.GET['filename']
|
||||
path = request.GET['path']
|
||||
file_path = os.path.join(str(BASE_DIR),path,filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
return HttpResponseRedirect('/api/clients')
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def add_peer(request):
|
||||
if request.method == 'POST':
|
||||
form = AddPeerForm(request.POST)
|
||||
if form.is_valid():
|
||||
rid = form.cleaned_data['clientID']
|
||||
uid = request.user.id
|
||||
username = form.cleaned_data['username']
|
||||
hostname = form.cleaned_data['hostname']
|
||||
plat = form.cleaned_data['platform']
|
||||
alias = form.cleaned_data['alias']
|
||||
tags = form.cleaned_data['tags']
|
||||
ip = form.cleaned_data['ip']
|
||||
|
||||
peer = RustDeskPeer(
|
||||
uid = uid,
|
||||
rid = rid,
|
||||
username = username,
|
||||
hostname = hostname,
|
||||
platform = plat,
|
||||
alias = alias,
|
||||
tags = tags,
|
||||
ip = ip
|
||||
)
|
||||
peer.save()
|
||||
return HttpResponseRedirect('/api/work')
|
||||
else:
|
||||
rid = request.GET.get('rid','')
|
||||
form = AddPeerForm()
|
||||
return render(request, 'add_peer.html', {'form': form, 'rid': rid, 'phone_or_desktop': is_mobile(request)})
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def edit_peer(request):
|
||||
if request.method == 'POST':
|
||||
form = EditPeerForm(request.POST)
|
||||
if form.is_valid():
|
||||
rid = form.cleaned_data['clientID']
|
||||
uid = request.user.id
|
||||
username = form.cleaned_data['username']
|
||||
hostname = form.cleaned_data['hostname']
|
||||
plat = form.cleaned_data['platform']
|
||||
alias = form.cleaned_data['alias']
|
||||
tags = form.cleaned_data['tags']
|
||||
|
||||
updated_peer = RustDeskPeer.objects.get(rid=rid,uid=uid)
|
||||
updated_peer.username=username
|
||||
updated_peer.hostname=hostname
|
||||
updated_peer.platform=plat
|
||||
updated_peer.alias=alias
|
||||
updated_peer.tags=tags
|
||||
updated_peer.save()
|
||||
|
||||
return HttpResponseRedirect('/api/work')
|
||||
else:
|
||||
print(form.errors)
|
||||
else:
|
||||
rid = request.GET.get('rid','')
|
||||
peer = RustDeskPeer.objects.get(rid=rid)
|
||||
initial_data = {
|
||||
'clientID': rid,
|
||||
'alias': peer.alias,
|
||||
'tags': peer.tags,
|
||||
'username': peer.username,
|
||||
'hostname': peer.hostname,
|
||||
'platform': peer.platform,
|
||||
'ip': peer.ip
|
||||
}
|
||||
form = EditPeerForm(initial=initial_data)
|
||||
return render(request, 'edit_peer.html', {'form': form, 'peer': peer, 'phone_or_desktop': is_mobile(request)})
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def assign_peer(request):
|
||||
if request.method == 'POST':
|
||||
form = AssignPeerForm(request.POST)
|
||||
if form.is_valid():
|
||||
rid = form.cleaned_data['clientID']
|
||||
uid = form.cleaned_data['uid']
|
||||
username = form.cleaned_data['username']
|
||||
hostname = form.cleaned_data['hostname']
|
||||
plat = form.cleaned_data['platform']
|
||||
alias = form.cleaned_data['alias']
|
||||
tags = form.cleaned_data['tags']
|
||||
ip = form.cleaned_data['ip']
|
||||
|
||||
peer = RustDeskPeer(
|
||||
uid = uid.id,
|
||||
rid = rid,
|
||||
username = username,
|
||||
hostname = hostname,
|
||||
platform = plat,
|
||||
alias = alias,
|
||||
tags = tags,
|
||||
ip = ip
|
||||
)
|
||||
peer.save()
|
||||
return HttpResponseRedirect('/api/work')
|
||||
else:
|
||||
print(form.errors)
|
||||
else:
|
||||
rid = request.GET.get('rid')
|
||||
form = AssignPeerForm()
|
||||
#get list of users from the database
|
||||
return render(request, 'assign_peer.html', {'form':form, 'rid': rid})
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def delete_peer(request):
|
||||
rid = request.GET.get('rid')
|
||||
peer = RustDeskPeer.objects.filter(Q(uid=request.user.id) & Q(rid=rid))
|
||||
peer.delete()
|
||||
return HttpResponseRedirect('/api/work')
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def conn_log(request):
|
||||
paginator = Paginator(get_conn_log(), 20)
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
return render(request, 'show_conn_log.html', {'page_obj':page_obj, 'phone_or_desktop': is_mobile(request)})
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def file_log(request):
|
||||
paginator = Paginator(get_file_log(), 20)
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
return render(request, 'show_file_log.html', {'page_obj':page_obj, 'phone_or_desktop': is_mobile(request)})
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def work(request):
|
||||
username = request.user
|
||||
u = UserProfile.objects.get(username=username)
|
||||
|
||||
show_type = request.GET.get('show_type', '')
|
||||
show_all = True if show_type == 'admin' and u.is_admin else False
|
||||
all_info, online_count_all = get_all_info()
|
||||
single_info, online_count_single = get_single_info(u.id)
|
||||
paginator = Paginator(all_info, 100) if show_type == 'admin' and u.is_admin else Paginator(single_info, 100)
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
return render(request, 'show_work.html', {'u':u, 'show_all':show_all, 'page_obj':page_obj, 'online_count_single':online_count_single, 'online_count_all':online_count_all, 'phone_or_desktop': is_mobile(request)})
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def down_peers(request):
|
||||
username = request.user
|
||||
u = UserProfile.objects.get(username=username)
|
||||
|
||||
if not u.is_admin:
|
||||
print(u.is_admin)
|
||||
return HttpResponseRedirect('/api/work')
|
||||
|
||||
all_info = get_all_info()
|
||||
f = xlwt.Workbook(encoding='utf-8')
|
||||
sheet1 = f.add_sheet(_(u'设备信息表'), cell_overwrite_ok=True)
|
||||
all_fields = [x.name for x in RustDesDevice._meta.get_fields()]
|
||||
all_fields.append('rust_user')
|
||||
for i, one in enumerate(all_info):
|
||||
for j, name in enumerate(all_fields):
|
||||
if i == 0:
|
||||
# 写入列名
|
||||
sheet1.write(i, j, name)
|
||||
sheet1.write(i+1, j, one.get(name, '-'))
|
||||
|
||||
sio = BytesIO()
|
||||
f.save(sio)
|
||||
sio.seek(0)
|
||||
response = HttpResponse(sio.getvalue(), content_type='application/vnd.ms-excel')
|
||||
response['Content-Disposition'] = 'attachment; filename=DeviceInfo.xls'
|
||||
response.write(sio.getvalue())
|
||||
return response
|
||||
|
||||
def check_sharelink_expired(sharelink):
|
||||
now = datetime.datetime.now()
|
||||
if sharelink.create_time > now:
|
||||
return False
|
||||
if (now - sharelink.create_time).seconds <15 * 60:
|
||||
return False
|
||||
else:
|
||||
sharelink.is_expired = True
|
||||
sharelink.save()
|
||||
return True
|
||||
|
||||
|
||||
@login_required(login_url='/api/user_action?action=login')
|
||||
def share(request):
|
||||
peers = RustDeskPeer.objects.filter(Q(uid=request.user.id))
|
||||
sharelinks = ShareLink.objects.filter(Q(uid=request.user.id) & Q(is_used=False) & Q(is_expired=False))
|
||||
|
||||
|
||||
# 省资源:处理已过期请求,不主动定时任务轮询请求,在任意地方请求时,检查是否过期,过期则保存。
|
||||
now = datetime.datetime.now()
|
||||
for sl in sharelinks:
|
||||
check_sharelink_expired(sl)
|
||||
sharelinks = ShareLink.objects.filter(Q(uid=request.user.id) & Q(is_used=False) & Q(is_expired=False))
|
||||
peers = [{'id':ix+1, 'name':f'{p.rid}|{p.alias}'} for ix, p in enumerate(peers)]
|
||||
sharelinks = [{'shash':s.shash, 'is_used':s.is_used, 'is_expired':s.is_expired, 'create_time':s.create_time, 'peers':s.peers} for ix, s in enumerate(sharelinks)]
|
||||
|
||||
if request.method == 'GET':
|
||||
url = request.build_absolute_uri()
|
||||
if url.endswith('share'):
|
||||
return render(request, 'share.html', {'peers':peers, 'sharelinks':sharelinks})
|
||||
else:
|
||||
shash = url.split('/')[-1]
|
||||
sharelink = ShareLink.objects.filter(Q(shash=shash))
|
||||
msg = ''
|
||||
title = 'success'
|
||||
if not sharelink:
|
||||
title = 'mistake'
|
||||
msg = f'Link{url}:<br>Share the link does not exist or have failed.'
|
||||
else:
|
||||
sharelink = sharelink[0]
|
||||
if str(request.user.id) == str(sharelink.uid):
|
||||
title = 'mistake'
|
||||
msg = f'Link{url}:<br><br>Lets say, you cant share the link to yourself, right?Intersection'
|
||||
else:
|
||||
sharelink.is_used = True
|
||||
sharelink.save()
|
||||
peers = sharelink.peers
|
||||
peers = peers.split(',')
|
||||
# 自己的peers若重叠,需要跳过
|
||||
peers_self_ids = [x.rid for x in RustDeskPeer.objects.filter(Q(uid=request.user.id))]
|
||||
peers_share = RustDeskPeer.objects.filter(Q(rid__in=peers) & Q(uid=sharelink.uid))
|
||||
peers_share_ids = [x.rid for x in peers_share]
|
||||
|
||||
for peer in peers_share:
|
||||
if peer.rid in peers_self_ids:
|
||||
continue
|
||||
#peer = RustDeskPeer.objects.get(rid=peer.rid)
|
||||
peer_f = RustDeskPeer.objects.filter(Q(rid=peer.rid) & Q(uid=sharelink.uid))
|
||||
if not peer_f:
|
||||
msg += f"{peer.rid}existed,"
|
||||
continue
|
||||
|
||||
if len(peer_f) > 1:
|
||||
msg += f'{peer.rid}There are multiple,Has skipped. '
|
||||
continue
|
||||
peer = peer_f[0]
|
||||
peer.id = None
|
||||
peer.uid = request.user.id
|
||||
peer.save()
|
||||
msg += f"{peer.rid},"
|
||||
|
||||
msg += 'Has been successfully obtained.'
|
||||
|
||||
title = _(title)
|
||||
msg = _(msg)
|
||||
return render(request, 'msg.html', {'title':msg, 'msg':msg})
|
||||
else:
|
||||
data = request.POST.get('data', '[]')
|
||||
|
||||
data = json.loads(data)
|
||||
if not data:
|
||||
return JsonResponse({'code':0, 'msg':_('数据为空。')})
|
||||
rustdesk_ids = [x['title'].split('|')[0] for x in data]
|
||||
rustdesk_ids = ','.join(rustdesk_ids)
|
||||
sharelink = ShareLink(
|
||||
uid=request.user.id,
|
||||
shash = getStrMd5(str(time.time())+salt),
|
||||
peers=rustdesk_ids,
|
||||
)
|
||||
sharelink.save()
|
||||
|
||||
return JsonResponse({'code':1, 'shash':sharelink.shash})
|
||||
|
||||
def is_mobile(request):
|
||||
user_agent = request.META['HTTP_USER_AGENT']
|
||||
if 'Mobile' in user_agent or 'Android' in user_agent or 'iPhone' in user_agent:
|
||||
return 'base_phone.html'
|
||||
else:
|
||||
return 'base.html'
|
343
api/views_generator.py
Normal file
4
db/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
18
docker-compose.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
rustdesk-api-server:
|
||||
container_name: rustdesk-api-server
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- TZ=Asia/Shanghai
|
||||
- CSRF_TRUSTED_ORIGINS=http://yourdomain.com:21114
|
||||
volumes:
|
||||
- /yourpath/db:/rustdesk-api-server/db
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "21114:21114"
|
||||
restart: unless-stopped
|
BIN
images/admin_devices.png
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
images/admin_main.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
images/admin_peers.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
images/admin_tags.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
images/admin_users.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
images/clients.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
images/connection_log.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
images/file_log.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
images/front_login.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
images/front_main.png
Normal file
After Width: | Height: | Size: 459 KiB |
BIN
images/front_reg.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
images/rust_books.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
images/share.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
images/user_devices.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
images/webui.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
images/windows_run.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
locale/en/LC_MESSAGES/django.mo
Normal file
592
locale/en/LC_MESSAGES/django.po
Normal file
@@ -0,0 +1,592 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-14 10:44+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\api\admin_user.py:14 .\api\front_locale.py:28
|
||||
msgid "密码"
|
||||
msgstr "Password"
|
||||
|
||||
#: .\api\admin_user.py:15
|
||||
msgid "再次输入密码"
|
||||
msgstr "Re-enter password"
|
||||
|
||||
#: .\api\admin_user.py:26
|
||||
msgid "密码校验失败,两次密码不一致。"
|
||||
msgstr "Password verification failed, passwords do not match."
|
||||
|
||||
#: .\api\admin_user.py:44
|
||||
msgid "密码Hash值"
|
||||
msgstr "Password Hash Value"
|
||||
|
||||
#: .\api\admin_user.py:75
|
||||
msgid "基本信息"
|
||||
msgstr "Basic Information"
|
||||
|
||||
#: .\api\admin_user.py:100
|
||||
msgid "RustDesk自建Web"
|
||||
msgstr "RustDesk Self-Hosted Web"
|
||||
|
||||
#: .\api\admin_user.py:101
|
||||
msgid "未定义"
|
||||
msgstr "Undefined"
|
||||
|
||||
#: .\api\front_locale.py:4 .\api\templates\base.html:42
|
||||
msgid "管理后台"
|
||||
msgstr "Admin Panel"
|
||||
|
||||
#: .\api\front_locale.py:5
|
||||
msgid "ID列表"
|
||||
msgstr "ID List"
|
||||
|
||||
#: .\api\front_locale.py:6
|
||||
msgid "分享机器"
|
||||
msgstr "Share Machine"
|
||||
|
||||
#: .\api\front_locale.py:7
|
||||
msgid "这么简易的东西,忘记密码这功能就没必要了吧。"
|
||||
msgstr ""
|
||||
"For such a simple thing, the forgot password feature is unnecessary, right?"
|
||||
|
||||
#: .\api\front_locale.py:8
|
||||
msgid "立即注册"
|
||||
msgstr "Register Now"
|
||||
|
||||
#: .\api\front_locale.py:9
|
||||
msgid "创建时间"
|
||||
msgstr "Creation Time"
|
||||
|
||||
#: .\api\front_locale.py:10
|
||||
msgid "注册成功,请前往登录页登录。"
|
||||
msgstr "Registration successful, please go to the login page to login."
|
||||
|
||||
#: .\api\front_locale.py:11
|
||||
msgid "注册日期"
|
||||
msgstr "Registration Date"
|
||||
|
||||
#: .\api\front_locale.py:12
|
||||
msgid ""
|
||||
"2、所分享的机器,被分享人享有相同的权限,如果机器设置了保存密码,被分享人也可"
|
||||
"以直接连接。"
|
||||
msgstr ""
|
||||
"2. The shared machine grants the same permissions to the recipient. If the "
|
||||
"machine is set to save password, the recipient can also connect directly."
|
||||
|
||||
#: .\api\front_locale.py:13
|
||||
msgid "导出xlsx"
|
||||
msgstr "Export as xlsx"
|
||||
|
||||
#: .\api\front_locale.py:14
|
||||
msgid "生成分享链接"
|
||||
msgstr "Generate Share Link"
|
||||
|
||||
#: .\api\front_locale.py:15
|
||||
msgid "请输入8~20位密码。可以包含字母、数字和特殊字符。"
|
||||
msgstr ""
|
||||
"Please enter a password of 8~20 characters. It can contain letters, numbers, "
|
||||
"and special characters."
|
||||
|
||||
#: .\api\front_locale.py:16
|
||||
msgid "尾页"
|
||||
msgstr "Last Page"
|
||||
|
||||
#: .\api\front_locale.py:17
|
||||
msgid "请确认密码"
|
||||
msgstr "Please confirm password"
|
||||
|
||||
#: .\api\front_locale.py:18
|
||||
msgid "注册"
|
||||
msgstr "Register"
|
||||
|
||||
#: .\api\front_locale.py:19 .\api\models_work.py:73
|
||||
msgid "内存"
|
||||
msgstr "Memory"
|
||||
|
||||
#: .\api\front_locale.py:20 .\api\templates\base.html:31
|
||||
msgid "首页"
|
||||
msgstr "Home"
|
||||
|
||||
#: .\api\front_locale.py:21 .\api\templates\base.html:37
|
||||
msgid "网页控制"
|
||||
msgstr "Web Control"
|
||||
|
||||
#: .\api\front_locale.py:22
|
||||
msgid "注册时间"
|
||||
msgstr "Registration Time"
|
||||
|
||||
#: .\api\front_locale.py:23
|
||||
msgid "链接地址"
|
||||
msgstr "Link Address"
|
||||
|
||||
#: .\api\front_locale.py:24
|
||||
msgid "请输入密码"
|
||||
msgstr "Please enter password"
|
||||
|
||||
#: .\api\front_locale.py:25 .\api\models_work.py:50 .\api\models_work.py:76
|
||||
msgid "系统用户名"
|
||||
msgstr "System Username"
|
||||
|
||||
#: .\api\front_locale.py:26
|
||||
msgid "状态"
|
||||
msgstr "Status"
|
||||
|
||||
#: .\api\front_locale.py:27
|
||||
msgid "已有账号?立即登录"
|
||||
msgstr "Already have an account? Login now"
|
||||
|
||||
#: .\api\front_locale.py:29 .\api\models_work.py:52
|
||||
msgid "别名"
|
||||
msgstr "Alias"
|
||||
|
||||
#: .\api\front_locale.py:30
|
||||
msgid "上一页"
|
||||
msgstr "Previous Page"
|
||||
|
||||
#: .\api\front_locale.py:31
|
||||
msgid "更新时间"
|
||||
msgstr "Update Time"
|
||||
|
||||
#: .\api\front_locale.py:32
|
||||
msgid "综合屏"
|
||||
msgstr "Comprehensive Screen"
|
||||
|
||||
#: .\api\front_locale.py:33 .\api\models_work.py:53
|
||||
msgid "平台"
|
||||
msgstr "Platform"
|
||||
|
||||
#: .\api\front_locale.py:34
|
||||
msgid "全部用户"
|
||||
msgstr "All Users"
|
||||
|
||||
#: .\api\front_locale.py:35
|
||||
msgid "注册页"
|
||||
msgstr "Registration Page"
|
||||
|
||||
#: .\api\front_locale.py:36
|
||||
msgid "分享机器给其他用户"
|
||||
msgstr "Share machine with other users"
|
||||
|
||||
#: .\api\front_locale.py:37 .\api\templates\base.html:33
|
||||
msgid "所有设备"
|
||||
msgstr "All Devices"
|
||||
|
||||
#: .\api\front_locale.py:38
|
||||
msgid "连接密码"
|
||||
msgstr "Connection Password"
|
||||
|
||||
#: .\api\front_locale.py:39
|
||||
msgid "设备统计"
|
||||
msgstr "Device Statistics"
|
||||
|
||||
#: .\api\front_locale.py:40
|
||||
msgid "所属用户"
|
||||
msgstr "Belongs to User"
|
||||
|
||||
#: .\api\front_locale.py:41 .\api\templates\base.html:36
|
||||
msgid "分享"
|
||||
msgstr "Share"
|
||||
|
||||
#: .\api\front_locale.py:42
|
||||
msgid "请输入用户名"
|
||||
msgstr "Please enter username"
|
||||
|
||||
#: .\api\front_locale.py:43
|
||||
msgid "1、链接有效期为15分钟,切勿随意分享给他人。"
|
||||
msgstr ""
|
||||
"1. The link is valid for 15 minutes. Do not share it with others casually."
|
||||
|
||||
#: .\api\front_locale.py:44
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: .\api\front_locale.py:45 .\api\models_work.py:49 .\api\models_work.py:70
|
||||
msgid "客户端ID"
|
||||
msgstr "Client ID"
|
||||
|
||||
#: .\api\front_locale.py:46
|
||||
msgid "下一页"
|
||||
msgstr "Next Page"
|
||||
|
||||
#: .\api\front_locale.py:47
|
||||
msgid "登录"
|
||||
msgstr "Login"
|
||||
|
||||
#: .\api\front_locale.py:48 .\api\templates\base.html:45
|
||||
msgid "退出"
|
||||
msgstr "Logout"
|
||||
|
||||
#: .\api\front_locale.py:49
|
||||
msgid "请将要分享的机器调整到右侧"
|
||||
msgstr "Please adjust the machines to be shared to the right"
|
||||
|
||||
#: .\api\front_locale.py:50
|
||||
msgid "成功!如需分享,请复制以下链接给其他人:<br>"
|
||||
msgstr ""
|
||||
"Success! If you need to share, please copy the following link to others:<br>"
|
||||
|
||||
#: .\api\front_locale.py:51
|
||||
msgid "忘记密码?"
|
||||
msgstr "Forgot Password?"
|
||||
|
||||
#: .\api\front_locale.py:52
|
||||
msgid "计算机名"
|
||||
msgstr "Computer Name"
|
||||
|
||||
#: .\api\front_locale.py:53
|
||||
msgid "两次输入密码不一致!"
|
||||
msgstr "Passwords do not match!"
|
||||
|
||||
#: .\api\front_locale.py:54
|
||||
msgid "页码"
|
||||
msgstr "Page Number"
|
||||
|
||||
#: .\api\front_locale.py:55
|
||||
msgid "版本"
|
||||
msgstr "Version"
|
||||
|
||||
#: .\api\front_locale.py:56 .\api\models_user.py:32 .\api\models_work.py:9
|
||||
msgid "用户名"
|
||||
msgstr "Username"
|
||||
|
||||
#: .\api\front_locale.py:57
|
||||
msgid ""
|
||||
"3、为保障安全,链接有效期为15分钟、链接仅有效1次。链接一旦被(非分享人的登录"
|
||||
"用户)访问,分享生效,后续访问链接失效。"
|
||||
msgstr ""
|
||||
"3. For security reasons, the link is valid for 15 minutes and only valid "
|
||||
"once. Once the link is accessed by a user (other than the sharing person), "
|
||||
"the sharing becomes effective, and subsequent access to the link will be "
|
||||
"invalid."
|
||||
|
||||
#: .\api\front_locale.py:58
|
||||
msgid "系统"
|
||||
msgstr "System"
|
||||
|
||||
#: .\api\front_locale.py:59
|
||||
msgid "我的机器"
|
||||
msgstr "My Machine"
|
||||
|
||||
#: .\api\front_locale.py:60
|
||||
#, fuzzy
|
||||
#| msgid "基本信息"
|
||||
msgid "信息"
|
||||
msgstr "Basic Information"
|
||||
|
||||
#: .\api\front_locale.py:61
|
||||
msgid "远程ID"
|
||||
msgstr "Remote ID"
|
||||
|
||||
#: .\api\front_locale.py:62
|
||||
msgid "远程别名"
|
||||
msgstr "Remote Alias"
|
||||
|
||||
#: .\api\front_locale.py:63 .\api\models_work.py:11 .\api\models_work.py:48
|
||||
#: .\api\models_work.py:126
|
||||
msgid "用户ID"
|
||||
msgstr "User ID"
|
||||
|
||||
#: .\api\front_locale.py:64
|
||||
msgid "用户别名"
|
||||
msgstr "User Alias"
|
||||
|
||||
#: .\api\front_locale.py:65
|
||||
msgid "用户IP"
|
||||
msgstr "User IP"
|
||||
|
||||
#: .\api\front_locale.py:66
|
||||
msgid "文件大小"
|
||||
msgstr "Filesize"
|
||||
|
||||
#: .\api\front_locale.py:67
|
||||
msgid "发送/接受"
|
||||
msgstr "Sent/Received"
|
||||
|
||||
#: .\api\front_locale.py:68
|
||||
msgid "记录于"
|
||||
msgstr "Logged At"
|
||||
|
||||
#: .\api\front_locale.py:69
|
||||
msgid "连接开始时间"
|
||||
msgstr "Connection Start Time\t"
|
||||
|
||||
#: .\api\front_locale.py:70
|
||||
msgid "连接结束时间"
|
||||
msgstr "Connection End Time"
|
||||
|
||||
#: .\api\front_locale.py:71
|
||||
msgid "时长"
|
||||
msgstr "Duration"
|
||||
|
||||
#: .\api\front_locale.py:72 .\api\templates\base.html:40
|
||||
msgid "连接日志"
|
||||
msgstr "Connection Log"
|
||||
|
||||
#: .\api\front_locale.py:73 .\api\templates\base.html:41
|
||||
msgid "文件传输日志"
|
||||
msgstr "File Transfer Log"
|
||||
|
||||
#: .\api\front_locale.py:74
|
||||
msgid "页码 #"
|
||||
msgstr "Page #"
|
||||
|
||||
#: .\api\front_locale.py:75
|
||||
msgid "下一页 #"
|
||||
msgstr "Next #"
|
||||
|
||||
#: .\api\front_locale.py:76
|
||||
msgid "上一页 #"
|
||||
msgstr "Previous #"
|
||||
|
||||
#: .\api\front_locale.py:77
|
||||
#, fuzzy
|
||||
#| msgid "上一页"
|
||||
msgid "第一页"
|
||||
msgstr "First"
|
||||
|
||||
#: .\api\front_locale.py:78
|
||||
#, fuzzy
|
||||
#| msgid "上一页"
|
||||
msgid "上页"
|
||||
msgstr "Previous Page"
|
||||
|
||||
#: .\api\models_user.py:40
|
||||
msgid "登录信息:"
|
||||
msgstr "Login Information:"
|
||||
|
||||
#: .\api\models_user.py:42
|
||||
msgid "是否激活"
|
||||
msgstr "Is Active"
|
||||
|
||||
#: .\api\models_user.py:43
|
||||
msgid "是否管理员"
|
||||
msgstr "Is Administrator"
|
||||
|
||||
#: .\api\models_user.py:82
|
||||
msgid "用户"
|
||||
msgstr "User"
|
||||
|
||||
#: .\api\models_user.py:83
|
||||
msgid "用户列表"
|
||||
msgstr "User List"
|
||||
|
||||
#: .\api\models_work.py:10
|
||||
msgid "RustDesk ID"
|
||||
msgstr "RustDesk ID"
|
||||
|
||||
#: .\api\models_work.py:12
|
||||
msgid "uuid"
|
||||
msgstr "UUID"
|
||||
|
||||
#: .\api\models_work.py:13
|
||||
msgid "access_token"
|
||||
msgstr "Access Token"
|
||||
|
||||
#: .\api\models_work.py:14
|
||||
msgid "登录时间"
|
||||
msgstr "Login Time"
|
||||
|
||||
#: .\api\models_work.py:19
|
||||
msgid "Token列表"
|
||||
msgstr "Token List"
|
||||
|
||||
#: .\api\models_work.py:30
|
||||
msgid "所属用户ID"
|
||||
msgstr "Belongs to User ID"
|
||||
|
||||
#: .\api\models_work.py:31
|
||||
msgid "标签名称"
|
||||
msgstr "Tag Name"
|
||||
|
||||
#: .\api\models_work.py:32
|
||||
msgid "标签颜色"
|
||||
msgstr "Tag Color"
|
||||
|
||||
#: .\api\models_work.py:37
|
||||
msgid "Tags列表"
|
||||
msgstr "Tags List"
|
||||
|
||||
#: .\api\models_work.py:51
|
||||
msgid "操作系统名"
|
||||
msgstr "Operating System Name"
|
||||
|
||||
#: .\api\models_work.py:54
|
||||
msgid "标签"
|
||||
msgstr "Tag"
|
||||
|
||||
#: .\api\models_work.py:55
|
||||
msgid "设备链接密码"
|
||||
msgstr "Device Connection Password"
|
||||
|
||||
#: .\api\models_work.py:60
|
||||
msgid "Peers列表"
|
||||
msgstr "Peers List"
|
||||
|
||||
#: .\api\models_work.py:72
|
||||
msgid "主机名"
|
||||
msgstr "Hostname"
|
||||
|
||||
#: .\api\models_work.py:74
|
||||
msgid "操作系统"
|
||||
msgstr "Operating System"
|
||||
|
||||
#: .\api\models_work.py:77
|
||||
msgid "客户端版本"
|
||||
msgstr "Client Version"
|
||||
|
||||
#: .\api\models_work.py:78
|
||||
msgid "设备注册时间"
|
||||
msgstr "Device Registration Time"
|
||||
|
||||
#: .\api\models_work.py:83
|
||||
msgid "设备"
|
||||
msgstr "Device"
|
||||
|
||||
#: .\api\models_work.py:84
|
||||
msgid "设备列表"
|
||||
msgstr "Device List"
|
||||
|
||||
#: .\api\models_work.py:127
|
||||
msgid "链接Key"
|
||||
msgstr "Link Key"
|
||||
|
||||
#: .\api\models_work.py:128
|
||||
msgid "机器ID列表"
|
||||
msgstr "Machine ID List"
|
||||
|
||||
#: .\api\models_work.py:129
|
||||
msgid "是否使用"
|
||||
msgstr "Is Used"
|
||||
|
||||
#: .\api\models_work.py:130
|
||||
msgid "是否过期"
|
||||
msgstr "Is Expired"
|
||||
|
||||
#: .\api\models_work.py:131
|
||||
msgid "生成时间"
|
||||
msgstr "Generation Time"
|
||||
|
||||
#: .\api\models_work.py:137
|
||||
msgid "分享链接"
|
||||
msgstr "Share Link"
|
||||
|
||||
#: .\api\models_work.py:138
|
||||
msgid "链接列表"
|
||||
msgstr "Link List"
|
||||
|
||||
#: .\api\views_api.py:20
|
||||
msgid "请求方式错误!请使用POST方式。"
|
||||
msgstr "Request method error! Please use the POST method."
|
||||
|
||||
#: .\api\views_api.py:34
|
||||
msgid "帐号或密码错误!请重试,多次重试后将被锁定IP!"
|
||||
msgstr ""
|
||||
"Account or password error! Please retry. After multiple retries, the IP will "
|
||||
"be locked!"
|
||||
|
||||
#: .\api\views_api.py:72
|
||||
msgid "请求方式错误!"
|
||||
msgstr "Request method error!"
|
||||
|
||||
#: .\api\views_api.py:80
|
||||
msgid "异常请求!"
|
||||
msgstr "Abnormal request!"
|
||||
|
||||
#: .\api\views_api.py:93 .\api\views_api.py:213
|
||||
msgid "错误的提交方式!"
|
||||
msgstr "Incorrect submission method!"
|
||||
|
||||
#: .\api\views_api.py:121
|
||||
msgid "拉取列表错误!"
|
||||
msgstr "Error fetching list!"
|
||||
|
||||
#: .\api\views_api.py:200
|
||||
msgid "更新地址簿有误"
|
||||
msgstr "Error updating address book"
|
||||
|
||||
#: .\api\views_api.py:247 .\api\views_front.py:207 .\api\views_front.py:226
|
||||
msgid "在线"
|
||||
msgstr "Online"
|
||||
|
||||
#: .\api\views_api.py:308
|
||||
msgid "好的"
|
||||
msgstr "Okay"
|
||||
|
||||
#: .\api\views_front.py:50
|
||||
msgid "model_to_dict接收的参数必须是模型对象"
|
||||
msgstr "The parameter received by model_to_dict must be a model object"
|
||||
|
||||
#: .\api\views_front.py:55
|
||||
#, python-brace-format
|
||||
msgid "model_to_dict,要替换成{replace_field}字段已经存在了"
|
||||
msgstr ""
|
||||
"model_to_dict, the field to be replaced with {replace_field} already exists"
|
||||
|
||||
#: .\api\views_front.py:60
|
||||
#, python-brace-format
|
||||
msgid "model_to_dict,要新增默认值,但字段{default_key}已经存在了"
|
||||
msgstr ""
|
||||
"model_to_dict, to add default values, but the field {default_key} already "
|
||||
"exists"
|
||||
|
||||
#: .\api\views_front.py:134
|
||||
msgid "出了点问题,未获取用户名或密码。"
|
||||
msgstr "There was a problem, username or password not obtained."
|
||||
|
||||
#: .\api\views_front.py:141
|
||||
msgid "帐号或密码错误!"
|
||||
msgstr "Account or password error!"
|
||||
|
||||
#: .\api\views_front.py:153
|
||||
msgid "当前未开放注册,请联系管理员!"
|
||||
msgstr "Registration is currently not open, please contact the administrator!"
|
||||
|
||||
#: .\api\views_front.py:160
|
||||
msgid "用户名不得小于3位"
|
||||
msgstr "Username must be at least 3 characters"
|
||||
|
||||
#: .\api\views_front.py:165
|
||||
msgid "密码长度不符合要求, 应在8~20位。"
|
||||
msgstr ""
|
||||
"Password length does not meet requirements, should be between 8~20 "
|
||||
"characters."
|
||||
|
||||
#: .\api\views_front.py:171
|
||||
msgid "用户名已存在。"
|
||||
msgstr "Username already exists."
|
||||
|
||||
#: .\api\views_front.py:207 .\api\views_front.py:226
|
||||
msgid "离线"
|
||||
msgstr "Offline"
|
||||
|
||||
#: .\api\views_front.py:210
|
||||
msgid "是"
|
||||
msgstr "Yes"
|
||||
|
||||
#: .\api\views_front.py:210
|
||||
msgid "否"
|
||||
msgstr "No"
|
||||
|
||||
#: .\api\views_front.py:252
|
||||
msgid "设备信息表"
|
||||
msgstr "Device Information Table"
|
||||
|
||||
#: .\api\views_front.py:351
|
||||
msgid "数据为空。"
|
||||
msgstr "Data is empty."
|
||||
|
||||
#: .\api\views_front.py:373 .\api\views_front.py:378 .\api\views_front.py:409
|
||||
#: .\api\views_front.py:414
|
||||
msgid "UNKNOWN"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "未知"
|
||||
#~ msgstr "UNKNOWN"
|
22
manage.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rustdesk_server_api.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
django
|
||||
xlwt
|
||||
python-dateutil
|
||||
psutil
|
||||
requests
|
||||
pillow
|
12
run.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /rustdesk-api-server;
|
||||
|
||||
if [ ! -e "./db/db.sqlite3" ]; then
|
||||
cp "./db_bak/db.sqlite3" "./db/db.sqlite3"
|
||||
echo "首次运行,初始化数据库"
|
||||
fi
|
||||
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py runserver $HOST:21114;
|
0
rustdesk_server_api/__init__.py
Normal file
16
rustdesk_server_api/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for rustdesk_server_api project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rustdesk_server_api.settings')
|
||||
|
||||
application = get_asgi_application()
|
177
rustdesk_server_api/settings.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Django settings for rustdesk_server_api project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.1.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
if "CSRF_TRUSTED_ORIGINS" in os.environ:
|
||||
CSRF_TRUSTED_ORIGINS = [os.environ["CSRF_TRUSTED_ORIGINS"]]
|
||||
else:
|
||||
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:21114"]
|
||||
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'None'
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY", 'j%7yjvygpih=6b%qf!q%&ixpn+27dngzdu-i3xh-^3xgy3^nnc')
|
||||
# ID服务器IP或域名,一般与中继服务器,用于web client
|
||||
ID_SERVER = os.environ.get("ID_SERVER", '')
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.environ.get("DEBUG", False)
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
AUTH_USER_MODEL = 'api.UserProfile' # AppName.自定义user
|
||||
|
||||
ALLOW_REGISTRATION = os.environ.get("ALLOW_REGISTRATION", "True") # 是否允许注册, True为允许,False为不允许
|
||||
ALLOW_REGISTRATION = True if ALLOW_REGISTRATION.lower() == 'true' else False
|
||||
|
||||
GHUSER = os.environ.get("GHUSER", '')
|
||||
GHBEARER = os.environ.get("GHBEARER", '')
|
||||
|
||||
|
||||
# ==========数据库配置 开始=====================
|
||||
DATABASE_TYPE = os.environ.get("DATABASE_TYPE", 'SQLITE')
|
||||
MYSQL_DBNAME = os.environ.get("MYSQL_DBNAME", '-')
|
||||
MYSQL_HOST = os.environ.get("MYSQL_HOST", '127.0.0.1')
|
||||
MYSQL_USER = os.environ.get("MYSQL_USER", '-')
|
||||
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD", '-')
|
||||
MYSQL_PORT = os.environ.get("MYSQL_PORT", '3306')
|
||||
# ==========数据库配置 结束=====================
|
||||
|
||||
LANGUAGE_CODE = os.environ.get("LANGUAGE_CODE", 'zh-hans')
|
||||
# #LANGUAGE_CODE = os.environ.get("LANGUAGE_CODE", 'en')
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'api',
|
||||
'webui',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
# 'django.middleware.csrf.CsrfViewMiddleware', # 取消post的验证。
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'rustdesk_server_api.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'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',
|
||||
'api.util.settings',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'rustdesk_server_api.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db/db.sqlite3',
|
||||
},
|
||||
}
|
||||
if DATABASE_TYPE == 'MYSQL' and MYSQL_DBNAME != '-' and MYSQL_USER != '-' and MYSQL_PASSWORD != '-':
|
||||
# 简单通过数据库名、账密信息过滤下,防止用户未配置mysql却使用mysql
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': MYSQL_DBNAME, # 数据库名
|
||||
'HOST': MYSQL_HOST, # 数据库服务器IP
|
||||
'USER': MYSQL_USER, # 数据库用户名
|
||||
'PASSWORD': MYSQL_PASSWORD, # 数据库密码
|
||||
'PORT': MYSQL_PORT, # 端口
|
||||
'OPTIONS': {'charset': 'utf8'},
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.1/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/3.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en'
|
||||
|
||||
TIME_ZONE = 'America/Chicago'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
# USE_TZ = True
|
||||
USE_TZ = False
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
if DEBUG:
|
||||
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
|
||||
|
||||
else:
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static') # 新增
|
||||
|
||||
LANGUAGES = (
|
||||
('zh-hans', '中文简体'),
|
||||
('en', 'English'),
|
||||
|
||||
)
|
||||
|
||||
LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
)
|
43
rustdesk_server_api/urls.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""rustdesk_server_api URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
import django
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from api.views import index
|
||||
if django.__version__.split('.')[0]>='4':
|
||||
from django.urls import re_path as url
|
||||
from django.conf.urls import include
|
||||
else:
|
||||
from django.conf.urls import url, include
|
||||
from django.views import static ##新增
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
path('admin/', admin.site.urls),
|
||||
url(r'^$', index),
|
||||
url(r'^api/', include('api.urls')),
|
||||
url(r'^webui/', include('webui.urls')),
|
||||
url(r'^static/(?P<path>.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'),
|
||||
url(r'^canvaskit@0.33.0/(?P<path>.*)$', static.serve, {'document_root': 'static/web_client/canvaskit@0.33.0'},name='web_client'),
|
||||
|
||||
]
|
||||
|
||||
from django.conf.urls import static as sc
|
||||
if not settings.DEBUG:
|
||||
urlpatterns += sc.static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
16
rustdesk_server_api/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for rustdesk_server_api project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rustdesk_server_api.settings')
|
||||
|
||||
application = get_wsgi_application()
|
275
static/admin/css/autocomplete.css
Normal file
@@ -0,0 +1,275 @@
|
||||
select.admin-autocomplete {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single,
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
||||
border-color: var(--body-quiet-color);
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
||||
color: var(--body-fg);
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 10px 5px 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
margin-top: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin: 5px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: var(--darkened-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: var(--body-quiet-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
||||
border: solid var(--body-quiet-color) 1px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown {
|
||||
background: var(--darkened-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
||||
background: var(--body-bg);
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
color: var(--body-fg);
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
color: var(--body-fg);
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
||||
background-color: var(--selected-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
1138
static/admin/css/base.css
Normal file
328
static/admin/css/changelists.css
Normal file
@@ -0,0 +1,328 @@
|
||||
/* CHANGELISTS */
|
||||
|
||||
#changelist {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#changelist .changelist-form-container {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#changelist table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.change-list .hiddenfields { display:none; }
|
||||
|
||||
.change-list .filtered table {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.change-list .filtered {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.change-list .filtered .results, .change-list .filtered .paginator,
|
||||
.filtered #toolbar, .filtered div.xfull {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.change-list .filtered table tbody th {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
#changelist-form .results {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist .toplinks {
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
#changelist .paginator {
|
||||
color: var(--body-quiet-color);
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
background: var(--body-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* CHANGELIST TABLES */
|
||||
|
||||
#changelist table thead th {
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#changelist table thead th.action-checkbox-column {
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#changelist table tbody td.action-checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#changelist table tfoot {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
/* TOOLBAR */
|
||||
|
||||
#toolbar {
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 15px;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
background: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#toolbar form input {
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
padding: 5px;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#toolbar #searchbar {
|
||||
height: 1.1875rem;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 5px;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
font-size: 0.8125rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#toolbar #searchbar:focus {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#toolbar form input[type="submit"] {
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.8125rem;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
background: var(--body-bg);
|
||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||
cursor: pointer;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#toolbar form input[type="submit"]:focus,
|
||||
#toolbar form input[type="submit"]:hover {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#changelist-search img {
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#changelist-search .help {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* FILTER COLUMN */
|
||||
|
||||
#changelist-filter {
|
||||
flex: 0 0 240px;
|
||||
order: 1;
|
||||
background: var(--darkened-bg);
|
||||
border-left: none;
|
||||
margin: 0 0 0 30px;
|
||||
}
|
||||
|
||||
#changelist-filter h2 {
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 5px 15px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist-filter h3,
|
||||
#changelist-filter details summary {
|
||||
font-weight: 400;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#changelist-filter details summary > * {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#changelist-filter details > summary {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#changelist-filter details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#changelist-filter details > summary::before {
|
||||
content: '→';
|
||||
font-weight: bold;
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
#changelist-filter details[open] > summary::before {
|
||||
content: '↓';
|
||||
}
|
||||
|
||||
#changelist-filter ul {
|
||||
margin: 5px 0;
|
||||
padding: 0 15px 15px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
#changelist-filter ul:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist-filter li {
|
||||
list-style-type: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#changelist-filter a {
|
||||
display: block;
|
||||
color: var(--body-quiet-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected {
|
||||
border-left: 5px solid var(--hairline-color);
|
||||
padding-left: 10px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected a {
|
||||
color: var(--link-selected-fg);
|
||||
}
|
||||
|
||||
#changelist-filter a:focus, #changelist-filter a:hover,
|
||||
#changelist-filter li.selected a:focus,
|
||||
#changelist-filter li.selected a:hover {
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
#changelist-filter #changelist-filter-clear a {
|
||||
font-size: 0.8125rem;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
/* DATE DRILLDOWN */
|
||||
|
||||
.change-list .toplinks {
|
||||
display: flex;
|
||||
padding-bottom: 5px;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px 17px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.change-list .toplinks a {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.change-list .toplinks .date-back {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.change-list .toplinks .date-back:focus,
|
||||
.change-list .toplinks .date-back:hover {
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
.filtered .actions {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#changelist table input {
|
||||
margin: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
|
||||
selector and the JS adding the class can be removed. */
|
||||
#changelist tbody tr.selected {
|
||||
background-color: var(--selected-row);
|
||||
}
|
||||
|
||||
#changelist tbody tr:has(.action-select:checked) {
|
||||
background-color: var(--selected-row);
|
||||
}
|
||||
|
||||
#changelist .actions {
|
||||
padding: 10px;
|
||||
background: var(--body-bg);
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
line-height: 1.5rem;
|
||||
color: var(--body-quiet-color);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist .actions span.all,
|
||||
#changelist .actions span.action-counter,
|
||||
#changelist .actions span.clear,
|
||||
#changelist .actions span.question {
|
||||
font-size: 0.8125rem;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
#changelist .actions:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
vertical-align: top;
|
||||
height: 1.5rem;
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
padding: 0 0 0 4px;
|
||||
margin: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#changelist .actions select:focus {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
#changelist .actions .button {
|
||||
font-size: 0.8125rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--body-bg);
|
||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||
cursor: pointer;
|
||||
height: 1.5rem;
|
||||
line-height: 1;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#changelist .actions .button:focus, #changelist .actions .button:hover {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
137
static/admin/css/dark_mode.css
Normal file
@@ -0,0 +1,137 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary: #264b5d;
|
||||
--primary-fg: #f7f7f7;
|
||||
|
||||
--body-fg: #eeeeee;
|
||||
--body-bg: #121212;
|
||||
--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: #212121;
|
||||
--selected-bg: #1b1b1b;
|
||||
--selected-row: #00363a;
|
||||
|
||||
--close-button-bg: #333333;
|
||||
--close-button-hover-bg: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--primary: #264b5d;
|
||||
--primary-fg: #f7f7f7;
|
||||
|
||||
--body-fg: #eeeeee;
|
||||
--body-bg: #121212;
|
||||
--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: #212121;
|
||||
--selected-bg: #1b1b1b;
|
||||
--selected-row: #00363a;
|
||||
|
||||
--close-button-bg: #333333;
|
||||
--close-button-hover-bg: #666666;
|
||||
}
|
||||
|
||||
/* THEME SWITCH */
|
||||
.theme-toggle {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
vertical-align: middle;
|
||||
margin-inline-start: 5px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
vertical-align: middle;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Fully hide screen reader text so we only show the one matching the current
|
||||
theme.
|
||||
*/
|
||||
.theme-toggle .visually-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .theme-toggle .theme-label-when-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ICONS */
|
||||
.theme-toggle svg.theme-icon-when-auto,
|
||||
.theme-toggle svg.theme-icon-when-dark,
|
||||
.theme-toggle svg.theme-icon-when-light {
|
||||
fill: var(--header-link-color);
|
||||
color: var(--header-bg);
|
||||
}
|
||||
|
||||
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
color: var(--body-fg);
|
||||
background-color: var(--body-bg);
|
||||
}
|
29
static/admin/css/dashboard.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* DASHBOARD */
|
||||
.dashboard td, .dashboard th {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dashboard .module table th {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard .module table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard .module table td a {
|
||||
display: block;
|
||||
padding-right: .6em;
|
||||
}
|
||||
|
||||
/* RECENT ACTIONS MODULE */
|
||||
|
||||
.module ul.actionlist {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul.actionlist li {
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
20
static/admin/css/fonts.css
Normal file
@@ -0,0 +1,20 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Bold-webfont.woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Regular-webfont.woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Light-webfont.woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
530
static/admin/css/forms.css
Normal file
@@ -0,0 +1,530 @@
|
||||
@import url('widgets.css');
|
||||
|
||||
/* FORM ROWS */
|
||||
|
||||
.form-row {
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
font-size: 0.8125rem;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.form-row img, .form-row input {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form-row label input[type="checkbox"] {
|
||||
margin-top: 0;
|
||||
vertical-align: 0;
|
||||
}
|
||||
|
||||
form .form-row p {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-multiline > div {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* FORM LABELS */
|
||||
|
||||
label {
|
||||
font-weight: normal;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.required label, label.required {
|
||||
font-weight: bold;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
/* RADIO BUTTONS */
|
||||
|
||||
form div.radiolist div {
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
form div.radiolist.inline div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
form div.radiolist label {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
form div.radiolist input[type="radio"] {
|
||||
margin: -2px 4px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form ul.inline {
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form ul.inline li {
|
||||
float: left;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
/* ALIGNED FIELDSETS */
|
||||
|
||||
.aligned label {
|
||||
display: block;
|
||||
padding: 4px 10px 0 0;
|
||||
width: 160px;
|
||||
word-wrap: break-word;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aligned label:not(.vCheckboxLabel):after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 1.625rem;
|
||||
}
|
||||
|
||||
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
|
||||
padding: 6px 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.aligned ul label {
|
||||
display: inline;
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.aligned .form-row input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
form .aligned ul {
|
||||
margin-left: 160px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form .aligned p.help,
|
||||
form .aligned div.help {
|
||||
margin-top: 0;
|
||||
margin-left: 160px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
form .aligned p.date div.help.timezonewarning,
|
||||
form .aligned p.datetime div.help.timezonewarning,
|
||||
form .aligned p.time div.help.timezonewarning {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
form .aligned p.help:last-child,
|
||||
form .aligned div.help:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
form .aligned input + p.help,
|
||||
form .aligned textarea + p.help,
|
||||
form .aligned select + p.help,
|
||||
form .aligned input + div.help,
|
||||
form .aligned textarea + div.help,
|
||||
form .aligned select + div.help {
|
||||
margin-left: 160px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
form .aligned ul li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
form .aligned table p {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.aligned .vCheckboxLabel {
|
||||
float: none;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
padding: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.aligned .vCheckboxLabel + p.help,
|
||||
.aligned .vCheckboxLabel + div.help {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
|
||||
width: 610px;
|
||||
}
|
||||
|
||||
fieldset .fieldBox {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* WIDE FIELDSETS */
|
||||
|
||||
.wide label {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
form .wide p,
|
||||
form .wide ul.errorlist,
|
||||
form .wide input + p.help,
|
||||
form .wide input + div.help {
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
form .wide p.help,
|
||||
form .wide div.help {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
form div.help ul {
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
/* COLLAPSED FIELDSETS */
|
||||
|
||||
fieldset.collapsed * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset.collapsed h2, fieldset.collapsed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
fieldset.collapsed {
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
fieldset.collapsed h2 {
|
||||
background: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
fieldset .collapse-toggle {
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
fieldset.collapsed .collapse-toggle {
|
||||
background: transparent;
|
||||
display: inline;
|
||||
color: var(--link-fg);
|
||||
}
|
||||
|
||||
/* MONOSPACE TEXTAREAS */
|
||||
|
||||
fieldset.monospace textarea {
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
|
||||
/* SUBMIT ROW */
|
||||
|
||||
.submit-row {
|
||||
padding: 12px 14px 12px;
|
||||
margin: 0 0 20px;
|
||||
background: var(--darkened-bg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
body.popup .submit-row {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.submit-row input {
|
||||
height: 2.1875rem;
|
||||
line-height: 0.9375rem;
|
||||
}
|
||||
|
||||
.submit-row input, .submit-row a {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.submit-row input.default {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
display: block;
|
||||
background: var(--delete-button-bg);
|
||||
border-radius: 4px;
|
||||
padding: 0.625rem 0.9375rem;
|
||||
height: 0.9375rem;
|
||||
line-height: 0.9375rem;
|
||||
color: var(--button-fg);
|
||||
}
|
||||
|
||||
.submit-row a.closelink {
|
||||
display: inline-block;
|
||||
background: var(--close-button-bg);
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
height: 0.9375rem;
|
||||
line-height: 0.9375rem;
|
||||
color: var(--button-fg);
|
||||
}
|
||||
|
||||
.submit-row a.deletelink:focus,
|
||||
.submit-row a.deletelink:hover,
|
||||
.submit-row a.deletelink:active {
|
||||
background: var(--delete-button-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.submit-row a.closelink:focus,
|
||||
.submit-row a.closelink:hover,
|
||||
.submit-row a.closelink:active {
|
||||
background: var(--close-button-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* CUSTOM FORM FIELDS */
|
||||
|
||||
.vSelectMultipleField {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.vCheckboxField {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vDateField, .vTimeField {
|
||||
margin-right: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vDateField {
|
||||
min-width: 6.85em;
|
||||
}
|
||||
|
||||
.vTimeField {
|
||||
min-width: 4.7em;
|
||||
}
|
||||
|
||||
.vURLField {
|
||||
width: 30em;
|
||||
}
|
||||
|
||||
.vLargeTextField, .vXMLLargeTextField {
|
||||
width: 48em;
|
||||
}
|
||||
|
||||
.flatpages-flatpage #id_content {
|
||||
height: 40.2em;
|
||||
}
|
||||
|
||||
.module table .vPositiveSmallIntegerField {
|
||||
width: 2.2em;
|
||||
}
|
||||
|
||||
.vIntegerField {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.vBigIntegerField {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.vForeignKeyRawIdAdminField {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.vTextField, .vUUIDField {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
/* INLINES */
|
||||
|
||||
.inline-group {
|
||||
padding: 0;
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
|
||||
.inline-group thead th {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.inline-group .aligned label {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.inline-related {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inline-related h3 {
|
||||
margin: 0;
|
||||
color: var(--body-quiet-color);
|
||||
padding: 5px;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--darkened-bg);
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete label {
|
||||
margin-left: 2px;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.inline-related fieldset {
|
||||
margin: 0;
|
||||
background: var(--body-bg);
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-related fieldset.module h3 {
|
||||
margin: 0;
|
||||
padding: 2px 5px 3px 5px;
|
||||
font-size: 0.6875rem;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background: #bcd;
|
||||
color: var(--body-bg);
|
||||
}
|
||||
|
||||
.inline-group .tabular fieldset.module {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inline-related.tabular fieldset.module table {
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.last-related fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inline-group .tabular tr.has_original td {
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.inline-group .tabular tr td.original {
|
||||
padding: 2px 0 0 0;
|
||||
width: 0;
|
||||
_position: relative;
|
||||
}
|
||||
|
||||
.inline-group .tabular th.original {
|
||||
width: 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inline-group .tabular td.original p {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 1.1em;
|
||||
padding: 2px 9px;
|
||||
overflow: hidden;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: bold;
|
||||
color: var(--body-quiet-color);
|
||||
_width: 700px;
|
||||
}
|
||||
|
||||
.inline-group ul.tools {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.inline-group ul.tools li {
|
||||
display: inline;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.inline-group div.add-row,
|
||||
.inline-group .tabular tr.add-row td {
|
||||
color: var(--body-quiet-color);
|
||||
background: var(--darkened-bg);
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.inline-group .tabular tr.add-row td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.inline-group ul.tools a.add,
|
||||
.inline-group div.add-row a,
|
||||
.inline-group .tabular tr.add-row td a {
|
||||
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
|
||||
padding-left: 16px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* RELATED FIELD ADD ONE / LOOKUP */
|
||||
|
||||
.related-lookup {
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 14px;
|
||||
}
|
||||
|
||||
.related-lookup {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-image: url(../img/search.svg);
|
||||
}
|
||||
|
||||
form .related-widget-wrapper ul {
|
||||
display: inline-block;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.clearable-file-input input {
|
||||
margin-top: 0;
|
||||
}
|
61
static/admin/css/login.css
Normal file
@@ -0,0 +1,61 @@
|
||||
/* LOGIN FORM */
|
||||
|
||||
.login {
|
||||
background: var(--darkened-bg);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.login #header {
|
||||
height: auto;
|
||||
padding: 15px 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login #header h1 {
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login #header h1 a {
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
.login #content {
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
|
||||
.login #container {
|
||||
background: var(--body-bg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
width: 28em;
|
||||
min-width: 300px;
|
||||
margin: 100px auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.login .form-row {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.login .form-row label {
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.login .form-row #id_username, .login .form-row #id_password {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
padding: 1em 0 0 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login .password-reset-link {
|
||||
text-align: center;
|
||||
}
|
144
static/admin/css/nav_sidebar.css
Normal file
@@ -0,0 +1,144 @@
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.toggle-nav-sidebar {
|
||||
z-index: 20;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 23px;
|
||||
width: 23px;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--hairline-color);
|
||||
background-color: var(--body-bg);
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
color: var(--link-fg);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .toggle-nav-sidebar {
|
||||
border-left: 1px solid var(--hairline-color);
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.toggle-nav-sidebar:hover,
|
||||
.toggle-nav-sidebar:focus {
|
||||
background-color: var(--darkened-bg);
|
||||
}
|
||||
|
||||
#nav-sidebar {
|
||||
z-index: 15;
|
||||
flex: 0 0 275px;
|
||||
left: -276px;
|
||||
margin-left: -276px;
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid var(--hairline-color);
|
||||
background-color: var(--body-bg);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[dir="rtl"] #nav-sidebar {
|
||||
border-left: 1px solid var(--hairline-color);
|
||||
border-right: 0;
|
||||
left: 0;
|
||||
margin-left: 0;
|
||||
right: -276px;
|
||||
margin-right: -276px;
|
||||
}
|
||||
|
||||
.toggle-nav-sidebar::before {
|
||||
content: '\00BB';
|
||||
}
|
||||
|
||||
.main.shifted .toggle-nav-sidebar::before {
|
||||
content: '\00AB';
|
||||
}
|
||||
|
||||
.main > #nav-sidebar {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.main.shifted > #nav-sidebar {
|
||||
margin-left: 0;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
[dir="rtl"] .main.shifted > #nav-sidebar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#nav-sidebar .module th {
|
||||
width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#nav-sidebar .module th,
|
||||
#nav-sidebar .module caption {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
#nav-sidebar .module td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[dir="rtl"] #nav-sidebar .module th,
|
||||
[dir="rtl"] #nav-sidebar .module caption {
|
||||
padding-left: 8px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
#nav-sidebar .current-app .section:link,
|
||||
#nav-sidebar .current-app .section:visited {
|
||||
color: var(--header-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#nav-sidebar .current-model {
|
||||
background: var(--selected-row);
|
||||
}
|
||||
|
||||
.main > #nav-sidebar + .content {
|
||||
max-width: calc(100% - 23px);
|
||||
}
|
||||
|
||||
.main.shifted > #nav-sidebar + .content {
|
||||
max-width: calc(100% - 299px);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
#nav-sidebar, #toggle-nav-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main > #nav-sidebar + .content,
|
||||
.main.shifted > #nav-sidebar + .content {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-filter {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 5px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--darkened-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#nav-filter:focus {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#nav-filter.no-results {
|
||||
background: var(--message-error-bg);
|
||||
}
|
||||
|
||||
#nav-sidebar table {
|
||||
width: 100%;
|
||||
}
|
998
static/admin/css/responsive.css
Normal file
@@ -0,0 +1,998 @@
|
||||
/* Tablets */
|
||||
|
||||
input[type="submit"], button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
/* Basic */
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 10px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
#container {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 15px 20px 20px;
|
||||
}
|
||||
|
||||
div.breadcrumbs {
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
#header {
|
||||
flex-direction: column;
|
||||
padding: 15px 30px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#branding h1 {
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
#user-tools {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
line-height: 1.85;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#user-tools a {
|
||||
display: inline-block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
|
||||
.dashboard #content {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#content-related {
|
||||
margin-right: -290px;
|
||||
}
|
||||
|
||||
.colSM #content-related {
|
||||
margin-left: -290px;
|
||||
}
|
||||
|
||||
.colMS {
|
||||
margin-right: 290px;
|
||||
}
|
||||
|
||||
.colSM {
|
||||
margin-left: 290px;
|
||||
}
|
||||
|
||||
.dashboard .module table td a {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
td .changelink, td .addlink {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Changelist */
|
||||
|
||||
#toolbar {
|
||||
border: none;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#changelist-search > div {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
#changelist-search label {
|
||||
line-height: 1.375rem;
|
||||
}
|
||||
|
||||
#toolbar form #searchbar {
|
||||
flex: 1 0 auto;
|
||||
width: 0;
|
||||
height: 1.375rem;
|
||||
margin: 0 10px 0 6px;
|
||||
}
|
||||
|
||||
#toolbar form input[type=submit] {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
#changelist-search .quiet {
|
||||
width: 0;
|
||||
flex: 1 0 auto;
|
||||
margin: 5px 0 0 25px;
|
||||
}
|
||||
|
||||
#changelist .actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
#changelist .actions .button {
|
||||
min-width: 48px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
#changelist .actions span.all,
|
||||
#changelist .actions span.clear,
|
||||
#changelist .actions span.question,
|
||||
#changelist .actions span.action-counter {
|
||||
font-size: 0.6875rem;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
flex-basis: 200px;
|
||||
}
|
||||
|
||||
.change-list .filtered .results,
|
||||
.change-list .filtered .paginator,
|
||||
.filtered #toolbar,
|
||||
.filtered .actions,
|
||||
|
||||
#changelist .paginator {
|
||||
border-top-color: var(--hairline-color); /* XXX Is this used at all? */
|
||||
}
|
||||
|
||||
#changelist .results + .paginator {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-row input[type=text],
|
||||
.form-row input[type=password],
|
||||
.form-row input[type=email],
|
||||
.form-row input[type=url],
|
||||
.form-row input[type=tel],
|
||||
.form-row input[type=number],
|
||||
.form-row textarea,
|
||||
.form-row select,
|
||||
.form-row .vTextField {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
min-height: 2.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-row select {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.form-row select[multiple] {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
fieldset .fieldBox + .fieldBox {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.aligned label {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.aligned .related-lookup,
|
||||
.aligned .datetimeshortcuts,
|
||||
.aligned .related-lookup + strong {
|
||||
align-self: center;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.submit-row {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
padding: 10px 7px;
|
||||
}
|
||||
|
||||
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
/* Related widget */
|
||||
|
||||
.related-widget-wrapper {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link + .selector {
|
||||
max-width: calc(100% - 30px);
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
select + .related-widget-wrapper-link,
|
||||
.related-widget-wrapper-link + .related-widget-wrapper-link {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Selector */
|
||||
|
||||
.selector {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selector .selector-filter label {
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.selector .selector-filter input {
|
||||
width: auto;
|
||||
min-height: 0;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
width: auto;
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
width: 100%;
|
||||
flex: 1 0 auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
width: 26px;
|
||||
height: 52px;
|
||||
padding: 2px 0;
|
||||
margin: auto 15px;
|
||||
border-radius: 20px;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.selector-add, .selector-remove {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: 20px auto;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background-position: 0 -120px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background-position: 0 -80px;
|
||||
}
|
||||
|
||||
a.selector-chooseall, a.selector-clearall {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.stacked {
|
||||
flex-direction: column;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.stacked > * {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.stacked select {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stacked .selector-available, .stacked .selector-chosen {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.stacked ul.selector-chooser {
|
||||
width: 52px;
|
||||
height: 26px;
|
||||
padding: 0 2px;
|
||||
margin: 15px auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.stacked .selector-chooser li {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.stacked .selector-add, .stacked .selector-remove {
|
||||
background-size: 20px auto;
|
||||
}
|
||||
|
||||
.stacked .selector-add {
|
||||
background-position: 0 -40px;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add {
|
||||
background-position: 0 -40px;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -140px;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
|
||||
background-position: 0 -60px;
|
||||
}
|
||||
|
||||
.stacked .selector-remove {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -100px;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
|
||||
.help-tooltip, .selector .help-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.datetime input {
|
||||
width: 50%;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.datetime span {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.datetime .timezonewarning {
|
||||
display: block;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.datetimeshortcuts {
|
||||
color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */
|
||||
}
|
||||
|
||||
.form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.inline-group {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
ul.messagelist li {
|
||||
padding-left: 55px;
|
||||
background-position: 30px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.error {
|
||||
background-position: 30px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.warning {
|
||||
background-position: 30px 14px;
|
||||
}
|
||||
|
||||
/* Login */
|
||||
|
||||
.login #header {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.login #branding h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* GIS */
|
||||
|
||||
div.olMap {
|
||||
max-width: calc(100vw - 30px);
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.olMap + .clear_features {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Docs */
|
||||
|
||||
.module table.xfull {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre.literal-block {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Layout */
|
||||
|
||||
#header, #content, #footer {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#footer:empty {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.breadcrumbs {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
|
||||
.colMS, .colSM {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#content-related, .colSM #content-related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#content-related .module {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#content-related .module h2 {
|
||||
padding: 10px 15px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Changelist */
|
||||
|
||||
#changelist {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
flex: 1 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist .actions span {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
position: static;
|
||||
width: auto;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.object-tools {
|
||||
float: none;
|
||||
margin: 0 0 15px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.object-tools li {
|
||||
height: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.object-tools li + li {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
.form-row {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.aligned .form-row,
|
||||
.aligned .form-row > div {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.aligned .form-row > div {
|
||||
width: calc(100vw - 30px);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.vURLField {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
fieldset .fieldBox + .fieldBox {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
fieldset.collapsed .form-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aligned label {
|
||||
width: 100%;
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
.aligned label:after {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.aligned .form-row input,
|
||||
.aligned .form-row select,
|
||||
.aligned .form-row textarea {
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.aligned .checkbox-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.aligned .checkbox-row input {
|
||||
flex: 0 1 auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.aligned .vCheckboxLabel {
|
||||
flex: 1 0;
|
||||
padding: 1px 0 0 5px;
|
||||
}
|
||||
|
||||
.aligned label + p,
|
||||
.aligned label + div.help,
|
||||
.aligned label + div.readonly {
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.aligned p.file-upload {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
span.clearable-file-input {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
span.clearable-file-input label {
|
||||
font-size: 0.8125rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.aligned .timezonewarning {
|
||||
flex: 1 0 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
form .aligned .form-row div.help {
|
||||
width: 100%;
|
||||
margin: 5px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form .aligned ul,
|
||||
form .aligned ul.errorlist {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist {
|
||||
margin-top: 5px;
|
||||
margin-right: 15px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist:not(.inline) div + div {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Related widget */
|
||||
|
||||
.related-widget-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.related-widget-wrapper .selector {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.related-widget-wrapper > a {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.related-widget-wrapper .radiolist ~ a {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.related-widget-wrapper > select ~ a {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
select + .related-widget-wrapper-link,
|
||||
.related-widget-wrapper-link + .related-widget-wrapper-link {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
/* Selector */
|
||||
|
||||
.selector {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.selector > * {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
margin-bottom: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
max-height: 96px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
display: block;
|
||||
float: none;
|
||||
width: 52px;
|
||||
height: 26px;
|
||||
padding: 0 2px;
|
||||
margin: 15px auto 20px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background-position: 0 -40px;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -60px;
|
||||
}
|
||||
|
||||
/* Inlines */
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related {
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related .module {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row {
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related h3 {
|
||||
padding: 10px;
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 2px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
|
||||
float: none;
|
||||
flex: 1 1 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .aligned label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] div.add-row {
|
||||
margin-top: 15px;
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.inline-group div.add-row,
|
||||
.inline-group .tabular tr.add-row td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inline-group div.add-row a,
|
||||
.inline-group .tabular tr.add-row td a {
|
||||
display: block;
|
||||
padding: 8px 10px 8px 26px;
|
||||
background-position: 8px 9px;
|
||||
}
|
||||
|
||||
/* Submit row */
|
||||
|
||||
.submit-row {
|
||||
padding: 10px;
|
||||
margin: 0 0 15px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit-row input, .submit-row input.default, .submit-row a {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-row a.closelink {
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
ul.messagelist li {
|
||||
padding-left: 40px;
|
||||
background-position: 15px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.error {
|
||||
background-position: 15px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.warning {
|
||||
background-position: 15px 14px;
|
||||
}
|
||||
|
||||
/* Paginator */
|
||||
|
||||
.paginator .this-page, .paginator a:link, .paginator a:visited {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
/* Login */
|
||||
|
||||
body.login {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.login #container {
|
||||
width: auto;
|
||||
max-width: 480px;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
.login #header,
|
||||
.login #content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.login #content-main {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.login .form-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login .form-row + .form-row {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.login .form-row label {
|
||||
margin: 0 0 5px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
padding: 15px 0 0;
|
||||
}
|
||||
|
||||
.login br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login .submit-row input {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.errornote {
|
||||
margin: 0 0 20px;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Calendar and clock */
|
||||
|
||||
.calendarbox, .clockbox {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
border: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.calendarbox:before, .clockbox:before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.calendarbox > *, .clockbox > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.calendarbox > div:first-child {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.calendarbox .calendar, .clockbox h2 {
|
||||
border-radius: 4px 4px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendarbox .calendar-cancel, .clockbox .calendar-cancel {
|
||||
border-radius: 0 0 4px 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-shortcuts {
|
||||
padding: 10px 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 0.75rem;
|
||||
}
|
||||
|
||||
.calendar-shortcuts a {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.timelist a {
|
||||
background: var(--body-bg);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.calendar-cancel {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.clockbox h2 {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.calendar caption {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
||||
z-index: 1;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
/* History */
|
||||
|
||||
table#change-history tbody th, table#change-history tbody td {
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
table#change-history tbody th {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Docs */
|
||||
|
||||
table.model tbody th, table.model tbody td {
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
81
static/admin/css/responsive_rtl.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* TABLETS */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
[dir="rtl"] .colMS {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] #user-tools {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] #changelist .actions label {
|
||||
padding-left: 10px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] #changelist .actions select {
|
||||
margin-left: 0;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .change-list .filtered .results,
|
||||
[dir="rtl"] .change-list .filtered .paginator,
|
||||
[dir="rtl"] .filtered #toolbar,
|
||||
[dir="rtl"] .filtered div.xfull,
|
||||
[dir="rtl"] .filtered .actions,
|
||||
[dir="rtl"] #changelist-filter {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .inline-group ul.tools a.add,
|
||||
[dir="rtl"] .inline-group div.add-row a,
|
||||
[dir="rtl"] .inline-group .tabular tr.add-row td a {
|
||||
padding: 8px 26px 8px 10px;
|
||||
background-position: calc(100% - 8px) 9px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .related-widget-wrapper-link + .selector {
|
||||
margin-right: 0;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .selector .selector-filter label {
|
||||
margin-right: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .object-tools li {
|
||||
float: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .object-tools li + li {
|
||||
margin-left: 0;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .dashboard .module table td a {
|
||||
padding-left: 0;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* MOBILE */
|
||||
|
||||
@media (max-width: 767px) {
|
||||
[dir="rtl"] .aligned .related-lookup,
|
||||
[dir="rtl"] .aligned .datetimeshortcuts {
|
||||
margin-left: 0;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .aligned ul,
|
||||
[dir="rtl"] form .aligned ul.errorlist {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] #changelist-filter {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
288
static/admin/css/rtl.css
Normal file
@@ -0,0 +1,288 @@
|
||||
/* GLOBAL */
|
||||
|
||||
th {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.module h2, .module caption {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.module ul, .module ol {
|
||||
margin-left: 0;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
.viewlink, .addlink, .changelink {
|
||||
padding-left: 0;
|
||||
padding-right: 16px;
|
||||
background-position: 100% 1px;
|
||||
}
|
||||
|
||||
.deletelink {
|
||||
padding-left: 0;
|
||||
padding-right: 16px;
|
||||
background-position: 100% 1px;
|
||||
}
|
||||
|
||||
.object-tools {
|
||||
float: left;
|
||||
}
|
||||
|
||||
thead th:first-child,
|
||||
tfoot td:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
/* LAYOUT */
|
||||
|
||||
#user-tools {
|
||||
right: auto;
|
||||
left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.breadcrumbs {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#content-main {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#content-related {
|
||||
float: left;
|
||||
margin-left: -300px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.colMS {
|
||||
margin-left: 300px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* SORTABLE TABLES */
|
||||
|
||||
table thead th.sorted .sortoptions {
|
||||
float: left;
|
||||
}
|
||||
|
||||
thead th.sorted .text {
|
||||
padding-right: 0;
|
||||
padding-left: 42px;
|
||||
}
|
||||
|
||||
/* dashboard styles */
|
||||
|
||||
.dashboard .module table td a {
|
||||
padding-left: .6em;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
/* changelists styles */
|
||||
|
||||
.change-list .filtered table {
|
||||
border-left: none;
|
||||
border-right: 0px none;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
margin-left: 0;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected {
|
||||
border-left: none;
|
||||
padding-left: 10px;
|
||||
margin-left: 0;
|
||||
border-right: 5px solid var(--hairline-color);
|
||||
padding-right: 10px;
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
/* FORMS */
|
||||
|
||||
.aligned label {
|
||||
padding: 0 0 3px 1em;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.vDateField, .vTimeField {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.aligned .form-row input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
form .aligned ul {
|
||||
margin-right: 163px;
|
||||
padding-right: 10px;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
form ul.inline li {
|
||||
float: right;
|
||||
padding-right: 0;
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
form .aligned p.help,
|
||||
form .aligned div.help {
|
||||
margin-right: 160px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
form div.help ul,
|
||||
form .aligned .checkbox-row + .help,
|
||||
form .aligned p.date div.help.timezonewarning,
|
||||
form .aligned p.datetime div.help.timezonewarning,
|
||||
form .aligned p.time div.help.timezonewarning {
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
form .wide p.help, form .wide div.help {
|
||||
padding-left: 0;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
form .wide p,
|
||||
form .wide ul.errorlist,
|
||||
form .wide input + p.help,
|
||||
form .wide input + div.help {
|
||||
margin-right: 200px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.submit-row {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
fieldset .fieldBox {
|
||||
margin-left: 20px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.errorlist li {
|
||||
background-position: 100% 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.errornote {
|
||||
background-position: 100% 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* WIDGETS */
|
||||
|
||||
.calendarnav-previous {
|
||||
top: 0;
|
||||
left: auto;
|
||||
right: 10px;
|
||||
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous:focus,
|
||||
.calendarbox .calendarnav-previous:hover {
|
||||
background-position: 0 -45px;
|
||||
}
|
||||
|
||||
.calendarnav-next {
|
||||
top: 0;
|
||||
right: auto;
|
||||
left: 10px;
|
||||
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-next:focus,
|
||||
.calendarbox .calendarnav-next:hover {
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
|
||||
.calendar caption, .calendarbox h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selector {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -80px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -112px;
|
||||
}
|
||||
|
||||
a.selector-chooseall {
|
||||
background: url(../img/selector-icons.svg) right -128px no-repeat;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
|
||||
background-position: 100% -144px;
|
||||
}
|
||||
|
||||
a.selector-clearall {
|
||||
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
|
||||
}
|
||||
|
||||
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
||||
background-position: 0 -176px;
|
||||
}
|
||||
|
||||
.inline-deletelink {
|
||||
float: left;
|
||||
}
|
||||
|
||||
form .form-row p.datetime {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.related-widget-wrapper {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* MISC */
|
||||
|
||||
.inline-related h2, .inline-group h2 {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete {
|
||||
padding-right: 20px;
|
||||
padding-left: inherit;
|
||||
left: 10px;
|
||||
right: inherit;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete label {
|
||||
margin-left: inherit;
|
||||
margin-right: 2px;
|
||||
}
|
21
static/admin/css/vendor/select2/LICENSE-SELECT2.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
481
static/admin/css/vendor/select2/select2.css
vendored
Normal file
@@ -0,0 +1,481 @@
|
||||
.select2-container {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
vertical-align: middle; }
|
||||
.select2-container .select2-selection--single {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 28px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||
display: block;
|
||||
padding-left: 8px;
|
||||
padding-right: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-selection--single .select2-selection__clear {
|
||||
position: relative; }
|
||||
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
|
||||
padding-right: 8px;
|
||||
padding-left: 20px; }
|
||||
.select2-container .select2-selection--multiple {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
min-height: 32px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
padding-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-search--inline {
|
||||
float: left; }
|
||||
.select2-container .select2-search--inline .select2-search__field {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
font-size: 100%;
|
||||
margin-top: 5px;
|
||||
padding: 0; }
|
||||
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
|
||||
.select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -100000px;
|
||||
width: 100%;
|
||||
z-index: 1051; }
|
||||
|
||||
.select2-results {
|
||||
display: block; }
|
||||
|
||||
.select2-results__options {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
.select2-results__option {
|
||||
padding: 6px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-results__option[aria-selected] {
|
||||
cursor: pointer; }
|
||||
|
||||
.select2-container--open .select2-dropdown {
|
||||
left: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--above {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-search--dropdown {
|
||||
display: block;
|
||||
padding: 4px; }
|
||||
.select2-search--dropdown .select2-search__field {
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
.select2-search--dropdown.select2-search--hide {
|
||||
display: none; }
|
||||
|
||||
.select2-close-mask {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
z-index: 99;
|
||||
background-color: #fff;
|
||||
filter: alpha(opacity=0); }
|
||||
|
||||
.select2-hidden-accessible {
|
||||
border: 0 !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
-webkit-clip-path: inset(50%) !important;
|
||||
clip-path: inset(50%) !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
white-space: nowrap !important; }
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 28px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: text; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
width: 100%; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
margin-right: 10px;
|
||||
padding: 1px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #333; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||
border: solid black 1px;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa; }
|
||||
|
||||
.select2-container--default .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield; }
|
||||
|
||||
.select2-container--default .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--default .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-disabled=true] {
|
||||
color: #999; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-selected=true] {
|
||||
background-color: #ddd; }
|
||||
|
||||
.select2-container--default .select2-results__option .select2-results__option {
|
||||
padding-left: 1em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em; }
|
||||
|
||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #5897fb;
|
||||
color: white; }
|
||||
|
||||
.select2-container--default .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic .select2-selection--single {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
outline: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
.select2-container--classic .select2-selection--single:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 28px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-right: 10px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__arrow {
|
||||
background-color: #ddd;
|
||||
border: none;
|
||||
border-left: 1px solid #aaa;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
border: none;
|
||||
border-right: 1px solid #aaa;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--single {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
|
||||
background: transparent;
|
||||
border: none; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
|
||||
|
||||
.select2-container--classic .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
outline: 0; }
|
||||
.select2-container--classic .select2-selection--multiple:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
|
||||
display: none; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #555; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--multiple {
|
||||
border: 1px solid #5897fb; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--inline .select2-search__field {
|
||||
outline: 0;
|
||||
box-shadow: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid transparent; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--above {
|
||||
border-bottom: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--below {
|
||||
border-top: none; }
|
||||
|
||||
.select2-container--classic .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--classic .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--classic .select2-results__option[aria-disabled=true] {
|
||||
color: grey; }
|
||||
|
||||
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #3875d7;
|
||||
color: white; }
|
||||
|
||||
.select2-container--classic .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-dropdown {
|
||||
border-color: #5897fb; }
|
1
static/admin/css/vendor/select2/select2.min.css
vendored
Normal file
603
static/admin/css/widgets.css
Normal file
@@ -0,0 +1,603 @@
|
||||
/* SELECTOR (FILTER INTERFACE) */
|
||||
|
||||
.selector {
|
||||
width: 800px;
|
||||
float: left;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
width: 380px;
|
||||
height: 17.2em;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
width: 380px;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.selector-available h2, .selector-chosen h2 {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.selector-chosen .list-footer-display {
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
margin: 0 0 10px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
background: var(--primary);
|
||||
color: var(--header-link-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.selector-chosen .list-footer-display__clear {
|
||||
color: var(--breadcrumbs-fg);
|
||||
}
|
||||
|
||||
.selector-chosen h2 {
|
||||
background: var(--primary);
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
.selector .selector-available h2 {
|
||||
background: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
border: 1px solid var(--border-color);
|
||||
border-width: 0 1px;
|
||||
padding: 8px;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.625rem;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.selector .selector-filter label,
|
||||
.inline-group .aligned .selector .selector-filter label {
|
||||
float: left;
|
||||
margin: 7px 0 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selector .selector-available input,
|
||||
.selector .selector-chosen input {
|
||||
width: 320px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
align-self: center;
|
||||
width: 22px;
|
||||
background-color: var(--selected-bg);
|
||||
border-radius: 10px;
|
||||
margin: 0 5px;
|
||||
padding: 0;
|
||||
transform: translateY(-17px);
|
||||
}
|
||||
|
||||
.selector-chooser li {
|
||||
margin: 0;
|
||||
padding: 3px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
padding: 0 10px;
|
||||
margin: 0 0 10px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
.selector .selector-chosen--with-filtered select {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
height: 14em;
|
||||
}
|
||||
|
||||
.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selector-add, .selector-remove {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
text-indent: -3000px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.active.selector-add, .active.selector-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.active.selector-add:hover, .active.selector-remove:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -112px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -80px;
|
||||
}
|
||||
|
||||
a.selector-chooseall, a.selector-clearall {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
text-align: left;
|
||||
margin: 1px auto 3px;
|
||||
overflow: hidden;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
color: var(--body-quiet-color);
|
||||
text-decoration: none;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
|
||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||
color: var(--link-fg);
|
||||
}
|
||||
|
||||
a.active.selector-chooseall, a.active.selector-clearall {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.selector-chooseall {
|
||||
padding: 0 18px 0 0;
|
||||
background: url(../img/selector-icons.svg) right -160px no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
|
||||
background-position: 100% -176px;
|
||||
}
|
||||
|
||||
a.selector-clearall {
|
||||
padding: 0 0 0 18px;
|
||||
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
||||
background-position: 0 -144px;
|
||||
}
|
||||
|
||||
/* STACKED SELECTORS */
|
||||
|
||||
.stacked {
|
||||
float: left;
|
||||
width: 490px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stacked select {
|
||||
width: 480px;
|
||||
height: 10.1em;
|
||||
}
|
||||
|
||||
.stacked .selector-available, .stacked .selector-chosen {
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.stacked .selector-available {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stacked .selector-available input {
|
||||
width: 422px;
|
||||
}
|
||||
|
||||
.stacked ul.selector-chooser {
|
||||
height: 22px;
|
||||
width: 50px;
|
||||
margin: 0 0 10px 40%;
|
||||
background-color: #eee;
|
||||
border-radius: 10px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.stacked .selector-chooser li {
|
||||
float: left;
|
||||
padding: 3px 3px 3px 5px;
|
||||
}
|
||||
|
||||
.stacked .selector-chooseall, .stacked .selector-clearall {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stacked .selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -32px no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add {
|
||||
background-position: 0 -32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
|
||||
background-position: 0 -48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 0 no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove {
|
||||
background-position: 0 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
|
||||
background-position: 0 -16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector .help-icon {
|
||||
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: -2px 0 0 2px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.selector .selector-chosen .help-icon {
|
||||
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.selector .search-label-icon {
|
||||
background: url(../img/search.svg) 0 0 no-repeat;
|
||||
display: inline-block;
|
||||
height: 1.125rem;
|
||||
width: 1.125rem;
|
||||
}
|
||||
|
||||
/* DATE AND TIME */
|
||||
|
||||
p.datetime {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--body-quiet-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.datetime span {
|
||||
white-space: nowrap;
|
||||
font-weight: normal;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
||||
margin-left: 5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
table p.datetime {
|
||||
font-size: 0.6875rem;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .clock-icon {
|
||||
background: url(../img/icon-clock.svg) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.datetimeshortcuts a:focus .clock-icon,
|
||||
.datetimeshortcuts a:hover .clock-icon {
|
||||
background-position: 0 -16px;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .date-icon {
|
||||
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.datetimeshortcuts a:focus .date-icon,
|
||||
.datetimeshortcuts a:hover .date-icon {
|
||||
background-position: 0 -16px;
|
||||
}
|
||||
|
||||
.timezonewarning {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
/* URL */
|
||||
|
||||
p.url {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.url a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* FILE UPLOADS */
|
||||
|
||||
p.file-upload {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.file-upload a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.file-upload .deletelink {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.clearable-file-input label {
|
||||
color: var(--body-fg);
|
||||
font-size: 0.6875rem;
|
||||
display: inline;
|
||||
float: none;
|
||||
}
|
||||
|
||||
/* CALENDARS & CLOCKS */
|
||||
|
||||
.calendarbox, .clockbox {
|
||||
margin: 5px auto;
|
||||
font-size: 0.75rem;
|
||||
width: 19em;
|
||||
text-align: center;
|
||||
background: var(--body-bg);
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clockbox {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendar table {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calendar caption, .calendarbox h2 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
border-top: none;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
color: #333;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.calendar th {
|
||||
padding: 8px 5px;
|
||||
background: var(--darkened-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.calendar td {
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calendar td.selected a {
|
||||
background: var(--primary);
|
||||
color: var(--button-fg);
|
||||
}
|
||||
|
||||
.calendar td.nonday {
|
||||
background: var(--darkened-bg);
|
||||
}
|
||||
|
||||
.calendar td.today a {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.calendar td a, .timelist a {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
padding: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.calendar td a:focus, .timelist a:focus,
|
||||
.calendar td a:hover, .timelist a:hover {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendar td a:active, .timelist a:active {
|
||||
background: var(--header-bg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendarnav {
|
||||
font-size: 0.625rem;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
margin: 0;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.calendarnav a:link, #calendarnav a:visited,
|
||||
#calendarnav a:focus, #calendarnav a:hover {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.calendar-shortcuts {
|
||||
background: var(--body-bg);
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 0.6875rem;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
text-indent: -9999px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendarnav-previous {
|
||||
left: 10px;
|
||||
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous:focus,
|
||||
.calendarbox .calendarnav-previous:hover {
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
|
||||
.calendarnav-next {
|
||||
right: 10px;
|
||||
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-next:focus,
|
||||
.calendarbox .calendarnav-next:hover {
|
||||
background-position: 0 -45px;
|
||||
}
|
||||
|
||||
.calendar-cancel {
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
font-size: 0.75rem;
|
||||
background: #eee;
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.calendar-cancel:focus, .calendar-cancel:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.calendar-cancel a {
|
||||
color: black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul.timelist, .timelist li {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timelist a {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* EDIT INLINE */
|
||||
|
||||
.inline-deletelink {
|
||||
float: right;
|
||||
text-indent: -9999px;
|
||||
background: url(../img/inline-delete.svg) 0 0 no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
.inline-deletelink:focus, .inline-deletelink:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* RELATED WIDGET WRAPPER */
|
||||
.related-widget-wrapper {
|
||||
float: left; /* display properly in form rows with multiple fields */
|
||||
overflow: hidden; /* clear floated contents */
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link:link {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link:link:focus,
|
||||
.related-widget-wrapper-link:link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
select + .related-widget-wrapper-link,
|
||||
.related-widget-wrapper-link + .related-widget-wrapper-link {
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
/* GIS MAPS */
|
||||
.dj_map {
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
}
|
202
static/admin/fonts/LICENSE.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
3
static/admin/fonts/README.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Roboto webfont source: https://www.google.com/fonts/specimen/Roboto
|
||||
WOFF files extracted using https://github.com/majodev/google-webfonts-helper
|
||||
Weights used in this project: Light (300), Regular (400), Bold (700)
|
BIN
static/admin/fonts/Roboto-Bold-webfont.woff
Normal file
BIN
static/admin/fonts/Roboto-Light-webfont.woff
Normal file
BIN
static/admin/fonts/Roboto-Regular-webfont.woff
Normal file
20
static/admin/img/LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Code Charm Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
7
static/admin/img/README.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
All icons are taken from Font Awesome (http://fontawesome.io/) project.
|
||||
The Font Awesome font is licensed under the SIL OFL 1.1:
|
||||
- https://scripts.sil.org/OFL
|
||||
|
||||
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
|
||||
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
|
||||
in current folder).
|
14
static/admin/img/calendar-icons.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="15" height="60" viewBox="0 0 1792 7168" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<g id="previous">
|
||||
<path d="M1037 1395l102-102q19-19 19-45t-19-45l-307-307 307-307q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-454 454q-19 19-19 45t19 45l454 454q19 19 45 19t45-19zm627-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</g>
|
||||
<g id="next">
|
||||
<path d="M845 1395l454-454q19-19 19-45t-19-45l-454-454q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l307 307-307 307q-19 19-19 45t19 45l102 102q19 19 45 19t45-19zm819-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</g>
|
||||
</defs>
|
||||
<use xlink:href="#previous" x="0" y="0" fill="#333333" />
|
||||
<use xlink:href="#previous" x="0" y="1792" fill="#000000" />
|
||||
<use xlink:href="#next" x="0" y="3584" fill="#333333" />
|
||||
<use xlink:href="#next" x="0" y="5376" fill="#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
static/admin/img/gis/move_vertex_off.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#EBECE6" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9C9C9" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
static/admin/img/gis/move_vertex_on.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F1C02A" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9A741" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
static/admin/img/icon-addlink.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#70bf2b" d="M1600 796v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 331 B |
3
static/admin/img/icon-alert.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#efb80b" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 504 B |