Compare commits
6 Commits
4d492ded4e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
e537ec2ac0
|
|||
|
ec102dd1cc
|
|||
|
7bae0d12de
|
|||
|
dbfb38bb8a
|
|||
|
20239242ce
|
|||
|
60e13822ee
|
38
AGENTS.md
38
AGENTS.md
@@ -209,6 +209,11 @@ labhelper/
|
||||
│ │ ├── clean_orphaned_files.py # Cleanup orphaned ThingFile attachments
|
||||
│ │ └── clean_orphaned_images.py # Cleanup orphaned Thing images
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ ├── static/
|
||||
│ │ └── css/
|
||||
│ │ ├── base.css # Base styles (layout, navbar, buttons, alerts, etc.)
|
||||
│ │ ├── edit_thing.css # Edit thing form styles
|
||||
│ │ └── thing_detail.css # Markdown content + lightbox styles
|
||||
│ ├── templates/
|
||||
│ │ └── boxes/
|
||||
│ │ ├── add_things.html # Form to add multiple things
|
||||
@@ -356,14 +361,21 @@ The project uses a base template system at `labhelper/templates/base.html`. All
|
||||
|
||||
### CSS Guidelines
|
||||
|
||||
**Static CSS Files:**
|
||||
- Base styles live in `boxes/static/css/base.css` (loaded by `base.html` via `{% static %}`)
|
||||
- Page-specific styles live in separate static CSS files (e.g., `thing_detail.css`, `edit_thing.css`)
|
||||
- Child templates load their CSS via `{% block extra_css %}` with `<link>` tags
|
||||
- WhiteNoise serves and cache-busts static files via `CompressedManifestStaticFilesStorage`
|
||||
- Run `python manage.py collectstatic` after adding or modifying static CSS files
|
||||
|
||||
**Naming:**
|
||||
- Use descriptive class names
|
||||
- BEM pattern encouraged for complex components
|
||||
- Inline styles allowed for template-specific styling
|
||||
- Inline styles allowed for template-specific one-off styling
|
||||
|
||||
**Styles:**
|
||||
- Use base template styles when possible
|
||||
- Template-specific styles in `{% block extra_css %}`
|
||||
- Use base CSS classes when possible
|
||||
- Page-specific styles in dedicated static CSS files loaded via `{% block extra_css %}`
|
||||
- JavaScript in `{% block extra_js %}`
|
||||
- Smooth transitions (0.2s - 0.3s)
|
||||
- Hover effects with transform and box-shadow
|
||||
@@ -410,28 +422,36 @@ The project uses a base template system at `labhelper/templates/base.html`. All
|
||||
- `title`: Page title tag
|
||||
- `page_header`: Page header with breadcrumbs
|
||||
- `content`: Main page content
|
||||
- `extra_css`: Additional styles
|
||||
- `extra_css`: Additional CSS via `<link>` tags to static files
|
||||
- `extra_head`: Additional head elements
|
||||
- `extra_js`: Additional JavaScript
|
||||
|
||||
3. **Load required template tags**
|
||||
```django
|
||||
{% load static %}
|
||||
{% load static %} {# Required when using {% static %} for CSS/asset links #}
|
||||
{% load mptt_tags %}
|
||||
{% load thumbnail %}
|
||||
{% load dict_extras %}
|
||||
```
|
||||
|
||||
4. **Use URL names for links**
|
||||
4. **Link page-specific CSS from static files**
|
||||
```django
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/thing_detail.css' %}">
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
5. **Use URL names for links**
|
||||
```django
|
||||
<a href="{% url 'box_detail' box.id %}">
|
||||
```
|
||||
|
||||
5. **Use icons with Font Awesome**
|
||||
6. **Use icons with Font Awesome**
|
||||
```django
|
||||
<i class="fas fa-box"></i>
|
||||
```
|
||||
|
||||
6. **Add breadcrumbs for navigation**
|
||||
7. **Add breadcrumbs for navigation**
|
||||
```django
|
||||
<p class="breadcrumb">
|
||||
<a href="/"><i class="fas fa-home"></i> Home</a> /
|
||||
@@ -439,7 +459,7 @@ The project uses a base template system at `labhelper/templates/base.html`. All
|
||||
</p>
|
||||
```
|
||||
|
||||
7. **Icon alignment in lists**: When using icons in list items, use fixed width containers to ensure proper alignment
|
||||
8. **Icon alignment in lists**: When using icons in list items, use fixed width containers to ensure proper alignment
|
||||
```django
|
||||
<a href="{% url 'thing_detail' thing.id %}" style="display: inline-block; width: 20px; text-align: center;">
|
||||
<i class="fas fa-link"></i>
|
||||
|
||||
@@ -17,6 +17,5 @@ data:
|
||||
LOGIN_URL: "login"
|
||||
LOGIN_REDIRECT_URL: "index"
|
||||
LOGOUT_REDIRECT_URL: "login"
|
||||
TRUSTED_PROXIES: "192.168.17.44,192.168.17.53"
|
||||
GUNICORN_OPTS: "--access-logfile -"
|
||||
IMAGE_TAG: "0.071"
|
||||
IMAGE_TAG: "0.076"
|
||||
|
||||
@@ -27,7 +27,7 @@ spec:
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: web
|
||||
image: git.baumann.gr/adebaumann/labhelper:0.071
|
||||
image: git.baumann.gr/adebaumann/labhelper:0.076
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
@@ -102,11 +102,6 @@ spec:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: LOGOUT_REDIRECT_URL
|
||||
- name: TRUSTED_PROXIES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: TRUSTED_PROXIES
|
||||
- name: GUNICORN_OPTS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
383
boxes/static/css/base.css
Normal file
383
boxes/static/css/base.css
Normal file
@@ -0,0 +1,383 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 15px 30px;
|
||||
margin: 20px auto;
|
||||
max-width: 1200px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.navbar-brand i {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #555;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-toggle:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-nav a,
|
||||
.navbar-nav form {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.navbar-nav a:hover,
|
||||
.navbar-nav button:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.navbar-nav a i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navbar-nav button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #555;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: none;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.navbar-nav.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar-nav a,
|
||||
.navbar-nav form {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.navbar-nav a:first-child,
|
||||
.navbar-nav form:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.navbar-nav > a:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.dropdown-content a:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.navbar-nav button {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: #333;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.page-header .breadcrumb {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-header .breadcrumb a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-header .breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #667eea;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 3px solid #667eea;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section h2 i {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #7f8c8d 0%, #95a5a6 100%);
|
||||
box-shadow: 0 4px 15px rgba(127, 140, 141, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
box-shadow: 0 6px 20px rgba(127, 140, 141, 0.6);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0, 184, 148, 0.3);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: white;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 10px 0;
|
||||
z-index: 1000;
|
||||
top: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dropdown-content a:first-child {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.dropdown-content a:last-child {
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
#logout-form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-content,
|
||||
.dropdown:focus-within .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdown-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dropdown {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-content,
|
||||
.dropdown:hover .dropdown-content,
|
||||
.dropdown:focus-within .dropdown-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
21
boxes/static/css/edit_thing.css
Normal file
21
boxes/static/css/edit_thing.css
Normal file
@@ -0,0 +1,21 @@
|
||||
#id_name, #id_description {
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
background: white;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
#id_name:focus, #id_description:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
#id_description {
|
||||
min-height: 120px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.detail-row {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
115
boxes/static/css/thing_detail.css
Normal file
115
boxes/static/css/thing_detail.css
Normal file
@@ -0,0 +1,115 @@
|
||||
.markdown-content p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
.markdown-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown-content h1, .markdown-content h2, .markdown-content h3,
|
||||
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
|
||||
margin: 1.5em 0 0.5em 0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-content h1:first-child, .markdown-content h2:first-child,
|
||||
.markdown-content h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown-content ul, .markdown-content ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.markdown-content li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
.markdown-content code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.markdown-content pre {
|
||||
background: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.markdown-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #667eea;
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 1em;
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
}
|
||||
.markdown-content a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
.markdown-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.markdown-content th, .markdown-content td {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-content th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
.lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 9999;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
.lightbox.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.lightbox img {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 40px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load thumbnail %}
|
||||
{% load dict_extras %}
|
||||
|
||||
@@ -286,29 +287,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
#id_name, #id_description {
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
background: white;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
#id_name:focus, #id_description:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
#id_description {
|
||||
min-height: 120px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.detail-row {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{% static 'css/edit_thing.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load thumbnail %}
|
||||
{% load dict_extras %}
|
||||
|
||||
@@ -169,121 +170,5 @@ $(document).ready(function() {
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.markdown-content p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
.markdown-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown-content h1, .markdown-content h2, .markdown-content h3,
|
||||
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
|
||||
margin: 1.5em 0 0.5em 0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-content h1:first-child, .markdown-content h2:first-child,
|
||||
.markdown-content h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown-content ul, .markdown-content ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.markdown-content li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
.markdown-content code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.markdown-content pre {
|
||||
background: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.markdown-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #667eea;
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 1em;
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
}
|
||||
.markdown-content a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
.markdown-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.markdown-content th, .markdown-content td {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-content th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
.lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 9999;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
.lightbox.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.lightbox img {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 40px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{% static 'css/thing_detail.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from gunicorn.glogging import Logger
|
||||
|
||||
TRUSTED_PROXIES = {
|
||||
ip.strip()
|
||||
for ip in os.environ.get("TRUSTED_PROXIES", "").split(",")
|
||||
if ip.strip()
|
||||
}
|
||||
|
||||
|
||||
class HealthCheckFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
@@ -21,25 +14,5 @@ class CustomLogger(Logger):
|
||||
super().setup(cfg)
|
||||
self.access_log.addFilter(HealthCheckFilter())
|
||||
|
||||
def atoms(self, resp, req, environ, request_time):
|
||||
atoms = super().atoms(resp, req, environ, request_time)
|
||||
atoms["{client-ip}e"] = self._get_client_ip(environ)
|
||||
return atoms
|
||||
|
||||
@staticmethod
|
||||
def _get_client_ip(environ):
|
||||
remote_addr = environ.get("REMOTE_ADDR", "-")
|
||||
xff = environ.get("HTTP_X_FORWARDED_FOR", "")
|
||||
if not xff:
|
||||
return remote_addr
|
||||
# Walk the chain from right to left, skipping trusted proxies
|
||||
ips = [ip.strip() for ip in xff.split(",")]
|
||||
for ip in reversed(ips):
|
||||
if ip not in TRUSTED_PROXIES:
|
||||
return ip
|
||||
# All IPs in the chain are trusted; fall back to the leftmost
|
||||
return ips[0]
|
||||
|
||||
|
||||
logger_class = CustomLogger
|
||||
access_log_format = '%({client-ip}e)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
@@ -7,388 +7,8 @@
|
||||
<title>{% block title %}LabHelper{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 15px 30px;
|
||||
margin: 20px auto;
|
||||
max-width: 1200px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.navbar-brand i {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #555;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-toggle:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-nav a,
|
||||
.navbar-nav form {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.navbar-nav a:hover,
|
||||
.navbar-nav button:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.navbar-nav a i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navbar-nav button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #555;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: none;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.navbar-nav.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar-nav a,
|
||||
.navbar-nav form {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.navbar-nav a:first-child,
|
||||
.navbar-nav form:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.navbar-nav a:last-child,
|
||||
.navbar-nav form:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.navbar-nav button {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: #333;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.page-header .breadcrumb {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-header .breadcrumb a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-header .breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #667eea;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 3px solid #667eea;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section h2 i {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #7f8c8d 0%, #95a5a6 100%);
|
||||
box-shadow: 0 4px 15px rgba(127, 140, 141, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
box-shadow: 0 6px 20px rgba(127, 140, 141, 0.6);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0, 184, 148, 0.3);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: white;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 10px 0;
|
||||
z-index: 1000;
|
||||
top: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dropdown-content a:first-child {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.dropdown-content a:last-child {
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.dropdown-content button:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-content,
|
||||
.dropdown:focus-within .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdown-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dropdown-content {
|
||||
position: static;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</style>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
@@ -413,12 +33,12 @@
|
||||
<a href="/resources/"><i class="fas fa-folder-open"></i> Resources</a>
|
||||
<a href="/fixme/"><i class="fas fa-exclamation-triangle"></i> Fixme</a>
|
||||
<a href="/admin/"><i class="fas fa-cog"></i> Admin</a>
|
||||
<form method="post" action="{% url 'logout' %}" style="padding: 0; margin: 0; background: none; border: none;">
|
||||
<form method="post" action="{% url 'logout' %}" id="logout-form">
|
||||
{% csrf_token %}
|
||||
<button type="submit" style="width: 100%; text-align: left; padding: 12px 20px; background: none; border: none; color: #555; font: inherit; cursor: pointer; display: block; transition: all 0.2s ease;">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
<a href="#" onclick="document.getElementById('logout-form').submit(); return false;">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user