├── user_api ├── __init__.py ├── urls.py ├── serializers.py ├── const.py ├── utility.py └── views.py ├── config ├── tests │ ├── __init__.py │ ├── test_readme.md │ ├── test_setup.py │ └── test_views.py ├── gunicorn.config.py ├── docker-compose.yml ├── Dockerfile ├── .env ├── wsgi.py ├── urls.py └── settings.py ├── static └── ihr │ ├── js │ ├── main.js │ └── vendor │ │ ├── npm.js │ │ └── modernizr-2.8.3-respond-1.4.2.min.js │ ├── tile.png │ ├── favicon.ico │ ├── tile-wide.png │ ├── disco_AS35540.png │ ├── hegemony_AS32.png │ ├── hegemony_AS2497.png │ ├── apple-touch-icon.png │ ├── logo_ihr_website.png │ ├── tartiflette_AS174.png │ ├── tartiflette_AS7922.png │ ├── AS33667_localHegemony.png │ ├── favicon_package_v0.16.zip │ ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 │ ├── Melbourne_Singapore_median_expid1_diffrtt_time.png │ ├── browserconfig.xml │ └── css │ ├── main.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap-theme.min.css │ └── bootstrap-theme.css ├── templates ├── contact_form │ ├── contact_form_subject.txt │ ├── contact_form.txt │ ├── contact_form_sent.html │ └── contact_form.html ├── flatpages │ └── default.html ├── ihr │ ├── country_list.html │ ├── asn_list.html │ ├── delay_detail.html │ ├── disco_detail.html │ ├── index.html │ └── country_detail.html ├── base.html └── rest_framework │ └── api.html ├── tests.py ├── apps.py ├── requirements.txt ├── .gitignore ├── const.py ├── urls.py ├── README.md ├── serializers.py └── models.py /user_api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/ihr/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/contact_form/contact_form_subject.txt: -------------------------------------------------------------------------------- 1 | message from {{ name }} 2 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /templates/contact_form/contact_form.txt: -------------------------------------------------------------------------------- 1 | {{ name }} 2 | {{ email }} 3 | {{ body }} 4 | -------------------------------------------------------------------------------- /static/ihr/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/tile.png -------------------------------------------------------------------------------- /static/ihr/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/favicon.ico -------------------------------------------------------------------------------- /static/ihr/tile-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/tile-wide.png -------------------------------------------------------------------------------- /static/ihr/disco_AS35540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/disco_AS35540.png -------------------------------------------------------------------------------- /static/ihr/hegemony_AS32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/hegemony_AS32.png -------------------------------------------------------------------------------- /static/ihr/hegemony_AS2497.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/hegemony_AS2497.png -------------------------------------------------------------------------------- /static/ihr/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/apple-touch-icon.png -------------------------------------------------------------------------------- /static/ihr/logo_ihr_website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/logo_ihr_website.png -------------------------------------------------------------------------------- /static/ihr/tartiflette_AS174.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/tartiflette_AS174.png -------------------------------------------------------------------------------- /static/ihr/tartiflette_AS7922.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/tartiflette_AS7922.png -------------------------------------------------------------------------------- /static/ihr/AS33667_localHegemony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/AS33667_localHegemony.png -------------------------------------------------------------------------------- /static/ihr/favicon_package_v0.16.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/favicon_package_v0.16.zip -------------------------------------------------------------------------------- /templates/contact_form/contact_form_sent.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 |

Your message was sent.

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /static/ihr/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/ihr/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/ihr/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/ihr/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/ihr/Melbourne_Singapore_median_expid1_diffrtt_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-django/HEAD/static/ihr/Melbourne_Singapore_median_expid1_diffrtt_time.png -------------------------------------------------------------------------------- /user_api/urls.py: -------------------------------------------------------------------------------- 1 | from .views import UserView 2 | from rest_framework.routers import DefaultRouter 3 | 4 | router = DefaultRouter() 5 | 6 | router.register(r'user', UserView, basename='user') 7 | 8 | urlpatterns = router.urls 9 | -------------------------------------------------------------------------------- /config/gunicorn.config.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | bind = "0.0.0.0:8000" # Replace with your desired IP and port 4 | workers = multiprocessing.cpu_count() * 2 + 1 5 | worker_class = "gthread" 6 | threads = 2 7 | timeout = 120 8 | 9 | preload_app = True 10 | -------------------------------------------------------------------------------- /templates/flatpages/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |
5 | 6 |

{{ flatpage.title }}

7 |
8 | {{ flatpage.content }} 9 |
10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /config/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: ihr-django 2 | 3 | services: 4 | web: 5 | build: 6 | context: ./ 7 | dockerfile: Dockerfile 8 | command: gunicorn ihr.config.wsgi:application --bind 0.0.0.0:8000 9 | restart: unless-stopped 10 | ports: 11 | - 8000:8000 12 | env_file: 13 | - ./.env 14 | -------------------------------------------------------------------------------- /config/tests/test_readme.md: -------------------------------------------------------------------------------- 1 | # ihr-django tests 2 | 3 | The test folder has to be in the same root directory of the project or location would have to be specified when running 4 | 5 | To run test just input `python manage.py test` into the terminal or Command prompt 6 | 7 | Postgres and redis-server have to be running, as the App depends on both for storing data 8 | 9 | -------------------------------------------------------------------------------- /config/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM python:3.8-slim-buster 3 | 4 | # set work directory 5 | WORKDIR /usr/src/app 6 | 7 | # set environment variables 8 | ENV PYTHONDONTWRITEBYTECODE 1 9 | ENV PYTHONUNBUFFERED 1 10 | 11 | # install dependencies 12 | RUN pip install --upgrade pip 13 | COPY ./ihr/requirements.txt . 14 | RUN pip install -r requirements.txt 15 | 16 | # copy project 17 | COPY . . 18 | -------------------------------------------------------------------------------- /config/.env: -------------------------------------------------------------------------------- 1 | DEBUG=1 2 | SECRET_KEY=put_some_random_text_here 3 | RECAPTCHA_SECRET= 4 | EMAIL_HOST_PASSWORD='email@example.com' 5 | SITE_ID=1 6 | EMAIL_HOST=smtp.gmail.com 7 | EMAIL_HOST_USER=example@gmail.com 8 | DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] ihr.iijlab.net ihr-backend.iijlab.net ihr.live ihr-dev.iijlab.net www.ihr.live *.ihr.live 9 | DB_USER=django 10 | DB_NAME=ihr 11 | DB_PASSWORD=123password456 12 | DB_HOST=localhost 13 | DB_PORT=5432 14 | -------------------------------------------------------------------------------- /static/ihr/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for internetHealthReport project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "internetHealthReport.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /templates/contact_form/contact_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |

Contact Form

5 |

To send us a message fill out the below form.

6 |
{% csrf_token %} 7 |

Name:

8 |

Your e-mail:

9 |

Message:

10 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/ihr/country_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block body %} 5 | 6 |
7 |
8 |

{{ object_list|length }} Monitored Countries

9 |
10 |
11 | 16 |
17 |
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /static/ihr/js/vendor/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /apps.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.apps import AppConfig 3 | from django.db import ProgrammingError 4 | from django.db.models.signals import post_migrate 5 | from django_filters import rest_framework as filters 6 | 7 | class NoMarkupDjangoFilterBackend(filters.DjangoFilterBackend): 8 | def to_html(self, request, queryset, view): 9 | # We want this, but currently it incurs a huge performance penality on ChoiceFields with 1000+ choices 10 | return '' 11 | 12 | 13 | class IHRConfig(AppConfig): 14 | name = 'ihr' 15 | 16 | def ready(self): 17 | super(IHRConfig, self).ready() 18 | 19 | -------------------------------------------------------------------------------- /templates/ihr/asn_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block body %} 5 | 6 |
7 |
8 |

{{ object_list|length }} Monitored Networks

9 |
10 |
11 | 20 |
21 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /user_api/serializers.py: -------------------------------------------------------------------------------- 1 | from ..models import IHRUser, MonitoredASN, ASN 2 | from rest_framework import serializers 3 | from ..serializers import ASNSerializer 4 | 5 | class MonitoredASNSerializer(serializers.ModelSerializer): 6 | asnumber = serializers.IntegerField(source="asn.number", read_only=True) 7 | asname = serializers.CharField(source="asn.name", read_only=True) 8 | class Meta: 9 | model = MonitoredASN 10 | fields = ('notifylevel', 'asnumber', 'asname') 11 | 12 | class IHRUserSerializer(serializers.ModelSerializer): 13 | monitoredasn = MonitoredASNSerializer(source="monitoredasn_set", required=False, read_only=True, many=True) 14 | 15 | class Meta: 16 | model = IHRUser 17 | fields = ('email', 'monitoredasn') -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | """internetHealthReport URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from rest_framework import routers 18 | 19 | 20 | urlpatterns = [ 21 | url(r'^', include("ihr.urls")), 22 | url(r'^ihr/api/api-auth/', include('rest_framework.urls', namespace='rest_framework')), 23 | ] 24 | 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | arrow==1.2.3 3 | asgiref==3.4.1 4 | async-timeout==4.0.2 5 | certifi==2022.12.7 6 | charset-normalizer==3.3.2 7 | coreapi==2.3.3 8 | coreschema==0.0.4 9 | Django==2.2.27 10 | django-cache-machine==1.2.0 11 | django-contact==0.1.1 12 | django-contact-form==1.9 13 | django-cors-headers==3.10.1 14 | django-crispy-forms==1.13.0 15 | django-extensions==3.1.5 16 | django-filter==2.2.0 17 | django-model-utils==1.5.0 18 | django-utils-six==2.0 19 | djangorestframework==3.13.1 20 | drf-extensions==0.7.1 21 | drf-yasg==1.21.5 22 | gunicorn==21.2.0 23 | idna==3.4 24 | inflection==0.5.1 25 | itypes==1.2.0 26 | Jinja2==3.0.3 27 | Markdown==3.3.7 28 | MarkupSafe==2.0.1 29 | numpy==1.19.5 30 | packaging==21.3 31 | pandas==1.1.5 32 | psycopg2-binary==2.8.6 33 | pyparsing==3.0.9 34 | python-dateutil==2.8.2 35 | python-memcached==1.59 36 | pytz==2022.7.1 37 | redis==4.3.6 38 | requests==2.32.3 39 | ruamel.yaml==0.17.21 40 | ruamel.yaml.clib==0.2.7 41 | six==1.16.0 42 | sqlparse==0.4.3 43 | uritemplate==4.1.1 44 | urllib3==1.26.14 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /static/ihr/css/main.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* ========================================================================== 4 | Author's custom styles 5 | ========================================================================== */ 6 | /* Sticky footer styles 7 | -------------------------------------------------- */ 8 | html { 9 | position: relative; 10 | min-height: 100%; 11 | } 12 | body { 13 | /* Margin bottom by footer height */ 14 | margin-bottom: 120px; 15 | } 16 | .footer { 17 | position: absolute; 18 | bottom: 0; 19 | width: 100%; 20 | /* Set the fixed height of the footer here */ 21 | height: 60px; 22 | background-color: #333333; 23 | padding-top: 25px; 24 | padding-bottom: 15px; 25 | padding-left: 150px; 26 | padding-right: 150px; 27 | color: #ffffff; 28 | } 29 | 30 | .footer a { 31 | color: #dddddd; 32 | } 33 | 34 | .glyphicon-refresh-animate { 35 | -animation: spin .7s infinite linear; 36 | -webkit-animation: spin2 .7s infinite linear; 37 | } 38 | 39 | @-webkit-keyframes spin2 { 40 | from { -webkit-transform: rotate(0deg);} 41 | to { -webkit-transform: rotate(360deg);} 42 | } 43 | 44 | @keyframes spin { 45 | from { transform: scale(1) rotate(0deg);} 46 | to { transform: scale(1) rotate(360deg);} 47 | } 48 | 49 | div.modal-body { 50 | overflow-y: auto; 51 | max-height: none; 52 | padding: 15px; 53 | } 54 | 55 | a.help-btn{ 56 | font-size: 20px; 57 | color: black; 58 | float: right; 59 | } 60 | 61 | a.help-btn:hover{ 62 | text-decoration: none; 63 | } 64 | 65 | a.help-btn:visited { 66 | text-decoration: none; 67 | } 68 | 69 | a.help-btn:link { 70 | text-decoration: none; 71 | } 72 | 73 | a.help-btn:active { 74 | text-decoration: none; 75 | } 76 | -------------------------------------------------------------------------------- /user_api/const.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sites.shortcuts import get_current_site 2 | import urllib.parse 3 | import os 4 | 5 | 6 | def get_link_domain(): 7 | if os.environ.get("ENV") == "production": 8 | return "https://ihr.iijlab.net/ihr" 9 | else: 10 | return "http://localhost:8080" 11 | 12 | 13 | class ConfirmationEmail: 14 | def __init__(self, email, token, password_change=False): 15 | self.email = email 16 | self.token = urllib.parse.quote(token) 17 | self.query = "&active=true" if password_change else "" 18 | 19 | @property 20 | def PLAIN(self): 21 | return f''' 22 | Confirm your email address to get started with Internet Health Report 23 | 24 | Confirmed that {self.email} is your email address to access to the personalization panel. 25 | 26 | Confirm email address 27 | [{get_link_domain()}/en-us/account_activation?token={self.token}{self.query}] 28 | 29 | If you haven’t requested this email, you can safely ignore it. 30 | ''' 31 | 32 | @property 33 | def HTML(self): 34 | return '''HTML VERSION''' 35 | 36 | 37 | class ResetPasswordEmail: 38 | def __init__(self, email, token): 39 | self.email = email 40 | self.token = urllib.parse.quote(token) 41 | 42 | @property 43 | def PLAIN(self): 44 | return f''' 45 | You request a email reset at Internet Health Report 46 | 47 | click on the link to reset your password. 48 | 49 | [{get_link_domain()}/en-us/reset_password?token={self.token} 50 | 51 | If you haven’t requested this email, you can safely ignore it. 52 | ''' 53 | 54 | @property 55 | def HTML(self): 56 | return '''HTML VERSION''' 57 | 58 | 59 | class StrErrors: 60 | OK = "ok" 61 | GENERIC = "Try again later. If the error persist please contact the administrator" 62 | WRONG_DATA = "check your data e try again" 63 | DUPLICATED = "duplicated email" 64 | INVALID = "invalid" 65 | TRY_AGAIN = "try again" 66 | RECAPTCHA_MISCONFIGURATION = "google_token_verification misconfiguration" 67 | ASN_DOESNOT_EXIST = "one of the as you sent is not in our server" 68 | ALREADY_VALIDATED = "this user is already validated" 69 | 70 | class INPUT: 71 | ADD_MONITORING = "you must provide a non empty array of (asn, alertLevel)" 72 | DUPLICATED = "your input contains duplicated data. Please check and try again" 73 | -------------------------------------------------------------------------------- /templates/ihr/delay_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | 8 | {% block body %} 9 |
10 |
11 |

Delay change event #{{ delay_events.id }} (AS {{ delay_events.asn_id }})

12 |
13 |
14 | 15 |
16 |
17 |

{{ delay_events.nbdiscoprobes }} probes disconnected between {{delay_events.starttime}} and {{delay_events.endtime}}

18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 70 | 71 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /templates/ihr/disco_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | 8 | {% block body %} 9 |
10 |
11 |

Disconnection event #{{ disco_events.id }} ({{ disco_events.streamtype }} {{ disco_events.streamname }})

12 |
13 |
14 | 15 |
16 |
17 |

{{ disco_events.nbdiscoprobes }} probes disconnected between {{disco_events.starttime}} and {{disco_events.endtime}}

18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 70 | 71 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /config/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | # Import necessary modules and classes 2 | from rest_framework.test import APITestCase 3 | from django.urls import reverse 4 | from ihr import const 5 | 6 | from ihr.const import ConfirmationEmail, ChangePasswordEmail, StrErrors, Msg, POOL, std_response 7 | import redis 8 | 9 | # Establish a connection to Redis using a connection pool 10 | conn = redis.Redis(connection_pool=POOL) 11 | 12 | # Define a test setup class 13 | class TestSetUp(APITestCase, const.ConfirmationEmail): 14 | 15 | # Method to send a registration email for testing 16 | def post_emailForRegistration(self): 17 | self.sendemail_url= reverse('ihr:UserSendEmailListView') 18 | 19 | # Sample email data 20 | self.email_data={ 21 | "email":"email@gmail.com" 22 | } 23 | 24 | # Send a POST request to the email endpoint 25 | self.client.post(self.sendemail_url, self.email_data, format="json") 26 | 27 | # Method to retrieve the confirmation code from Redis 28 | def get_code(self): 29 | code = conn.get(f"Confirmation_{self.email_data['email']}") 30 | return code 31 | 32 | # Method to retrieve the forget password code from Redis 33 | def get_forget_code(self): 34 | code = conn.get(f"ChangePassword_{self.email_data['email']}") 35 | return code 36 | 37 | # Method to set up data and parameters before each test case 38 | def setUp(self): 39 | # URLs for various user-related actions 40 | self.register_url= reverse('ihr:UserRegisterListView') 41 | self.login_url= reverse('ihr:UserLoginListView') 42 | self.sendemail_url= reverse('ihr:UserSendEmailListView') 43 | self.logout_url= reverse('ihr:UserLogoutListView') 44 | self.change_password_url = reverse('ihr:UserChangePasswordListView') 45 | self.sendforgetpasswordemail_url = reverse('ihr:UserSendForgetPasswordEmailListView') 46 | self.forget_password_url = reverse('ihr:UserForgetPasswordListView') 47 | 48 | # Sample email data for testing 49 | self.email_data={ 50 | "email":"email@gmail.com" 51 | } 52 | 53 | # Sample user data for registration and login 54 | self.user_data={ 55 | "email":"email@gmail.com", 56 | "password":"password123", 57 | "code": self.get_code(), 58 | } 59 | 60 | # Sample data for changing password 61 | self.change_password_data={ 62 | "email":"email@gmail.com", 63 | "password":"password123", 64 | "new_password":"password1234", 65 | } 66 | 67 | # Sample data for forgetting password 68 | self.forget_password_data={ 69 | "email":"email@gmail.com", 70 | "new_password":"password1234", 71 | "code": self.get_forget_code(), 72 | } 73 | 74 | return super().setUp() 75 | # Method to clean up resources after each test case 76 | def tearDown(self): 77 | return super().tearDown() 78 | 79 | -------------------------------------------------------------------------------- /const.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sites.shortcuts import get_current_site 2 | from rest_framework.response import Response 3 | import urllib.parse 4 | import random 5 | import redis 6 | POOL = redis.ConnectionPool(host='127.0.0.1', port=6379,max_connections=100, decode_responses=True) 7 | conn = redis.Redis(connection_pool=POOL) 8 | 9 | from rest_framework.status import ( 10 | HTTP_500_INTERNAL_SERVER_ERROR, 11 | HTTP_401_UNAUTHORIZED, 12 | HTTP_200_OK 13 | ) 14 | 15 | class ConfirmationEmail: 16 | def __init__(self, email): 17 | self.email = email 18 | 19 | @property 20 | def creat_code(self, code_num: int = 6): 21 | base_code = ['0','1','2','3','4','5','6','7','8','9'] 22 | code_list = random.sample(base_code, code_num) 23 | code = ''.join(code_list) 24 | return code 25 | 26 | @property 27 | def PLAIN(self): 28 | self.code = self.creat_code 29 | print("self.code:",self.code) 30 | conn.set(f"Confirmation_{self.email}", self.code, ex=300) 31 | return f''' 32 | Confirm your email address to get started with Internet Health Report. 33 | Confirmed that {self.email} is your email address to access to the personalization panel. 34 | Confirm email code: {self.code}. 35 | If you haven’t requested this email, you can safely ignore it.''' 36 | 37 | class ChangePasswordEmail: 38 | def __init__(self, email): 39 | self.email = email 40 | 41 | @property 42 | def creat_code(self, code_num: int = 6): 43 | base_code = ['0','1','2','3','4','5','6','7','8','9'] 44 | code_list = random.sample(base_code, code_num) 45 | code = ''.join(code_list) 46 | return code 47 | 48 | @property 49 | def PLAIN(self): 50 | self.code = self.creat_code 51 | print("self.code:",self.code) 52 | conn.set(f"ChangePassword_{self.email}", self.code, ex=300) 53 | return f''' 54 | Confirm your email address to change password. 55 | Confirmed that {self.email} is your email address to access to the personalization panel. 56 | Confirm email code: {self.code}. 57 | If you haven’t requested this email, you can safely ignore it.''' 58 | 59 | def std_response(detail, status_code): 60 | """ 61 | shortener for error response into views 62 | """ 63 | return Response({'detail': detail}, status=status_code) 64 | 65 | class Msg: 66 | USER_ALREADY_REGISTERED = "User already exists!" 67 | USER_NOT_EXIST = "User does not exist" 68 | REGISTER_SUCCEEDED = "User registration succeeded!" 69 | LOGIN_SUCCEEDED = "User login succeeded!" 70 | LOGOUT_SUCCEEDED = "User logout succeeded!" 71 | LOGIN_FAILED = "User login failed, password error!" 72 | 73 | CHANGE_PASSWORD_SUCCEEDED = "User change password succeeded!" 74 | 75 | CODE_ERROR = "Verification code error!" 76 | CODE_SENT = "Verification code has been sent!" 77 | 78 | REQUEST_EXCEPTION = "Request exception!" 79 | 80 | SEARCH_SUCCEEDED = "User search succeeded!" 81 | SAVE_SUCCEEDED = "User save succeeded!" 82 | 83 | INVALID_DATA = "Invalid data!" 84 | 85 | 86 | class StrErrors: 87 | OK = "ok" 88 | GENERIC = "Try again later. If the error persist please contact the administrator" 89 | WRONG_DATA= "check your data and try again" 90 | DUPLICATED = "duplicated email" 91 | INVALID = "invalid" 92 | TRY_AGAIN = "try again" 93 | RECAPTCHA_MISCONFIGURATION = "google_token_verification misconfiguration" 94 | ASN_DOESNOT_EXIST = "one of the as you sent is not in our server" 95 | ALREADY_VALIDATED = "this user is already validated" 96 | class INPUT: 97 | ADD_MONITORING = "you must provide a non empty array of (asn, alertLevel)" 98 | DUPLICATED = "your input contains duplicated data. Please check and try again" -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}Internet Health Report{% endblock %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% block bootstrap %} 24 | 25 | 26 | {% endblock %} 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% block head %}{% endblock %} 40 | 41 | 42 | 45 | 58 | 59 |
60 | {% block body %} 61 | {% endblock %} 62 | 63 |
64 | 65 | 66 | 67 | 68 | {% block script %} 69 | {% endblock %} 70 | 71 | 72 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /user_api/utility.py: -------------------------------------------------------------------------------- 1 | from .const import StrErrors 2 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 3 | from rest_framework.response import Response 4 | import requests 5 | import json 6 | from django.conf import settings 7 | from rest_framework.views import exception_handler 8 | from rest_framework.status import ( 9 | HTTP_500_INTERNAL_SERVER_ERROR, 10 | HTTP_401_UNAUTHORIZED, 11 | HTTP_200_OK 12 | ) 13 | import logging 14 | logger = logging.getLogger(__name__) 15 | import traceback as tb 16 | 17 | def std_response(detail, status_code): 18 | """ 19 | shortener for error response into views 20 | """ 21 | return Response({'detail': detail}, status=status_code) 22 | 23 | 24 | class IHRException(Exception): 25 | """ 26 | Internal exception. You don't need to catch them they are automatically handled 27 | """ 28 | def __init__(self, status_code=HTTP_500_INTERNAL_SERVER_ERROR, message=StrErrors.GENERIC, severity=logging.CRITICAL, log=True, traceback=False, *extra): 29 | #extra arguments will be appendend to message 30 | self.message = message 31 | log = log or traceback 32 | for elem in extra: 33 | self.message += " " + elem 34 | super().__init__(self.message) 35 | if(log): 36 | logger.log(severity, self.message, tb.format_exc() if traceback else "") 37 | self.status_code = status_code 38 | #TODO something more accurate if you want 39 | 40 | def response(self): 41 | """ 42 | Modify the given response message adding custom fields 43 | """ 44 | return std_response(self.message, self.status_code) 45 | 46 | # a set of widely used exception 47 | @staticmethod 48 | def STD_INVALID(traceback=False): 49 | return IHRException(status_code=HTTP_401_UNAUTHORIZED, message=StrErrors.INVALID, traceback=traceback) 50 | 51 | def IHR_exception_handler(exc, context): 52 | """ 53 | Exception handler for internal errors 54 | see the link below for furter details 55 | https://www.django-rest-framework.org/api-guide/exceptions/ 56 | """ 57 | if isinstance(exc, IHRException): 58 | return exc.response() 59 | 60 | # default handler 61 | response = exception_handler(exc, context) 62 | 63 | return response 64 | 65 | 66 | class TokenGenerator(PasswordResetTokenGenerator): 67 | def _make_hash_value(self, user, timestamp): 68 | return (str(user.pk) + user.email + str(user.is_active) + str(timestamp)) 69 | # is active permit automatic token disabling on activation 70 | 71 | account_activation_token = TokenGenerator() 72 | 73 | class EmailChangeTokenGenerator(PasswordResetTokenGenerator): 74 | def __init__(self, new_email, request_time): 75 | super().__init__() 76 | self.new_email = new_email 77 | self.request_time = request_time 78 | 79 | def _make_hash_value(self, user, timestamp): 80 | print(str(user.pk), user.email, str(user.is_active), str(timestamp), self.new_email, str(self.request_time)) 81 | return (str(user.pk) + user.email + str(user.is_active) + str(timestamp) + self.new_email + str(self.request_time)) 82 | 83 | 84 | def parse_request(request): 85 | """ 86 | return the content of the request as a dictionary 87 | """ 88 | if isinstance(request.data, dict): 89 | return request.data 90 | return request.data.dict() 91 | 92 | def google_token_verification(request): 93 | """ 94 | verify the given google recaptcha token. 95 | """ 96 | try: 97 | content = json.loads(requests.post( 98 | 'https://www.google.com/recaptcha/api/siteverify', 99 | data={ 100 | 'secret': settings.RECAPTCHA_SECRET, 101 | 'response': request["recaptcha"] 102 | } 103 | ).content) 104 | print(content) 105 | try: 106 | if content['success']: 107 | return True 108 | 109 | if 'invalid-input-response' in content['error-codes']: 110 | raise IHRException.STD_INVALID() 111 | if 'timeout-or-duplicate' in content['error-codes']: 112 | raise IHRException(status_code=HTTP_401_UNAUTHORIZED, message=StrErrors.TRY_AGAIN, log=False) 113 | 114 | raise IHRException(*content['error-codes'], message=StrErrors.RECAPTCHA_MISCONFIGURATION) 115 | except KeyError as e: 116 | raise IHRException(str(e), message=StrErrors.RECAPTCHA_MISCONFIGURATION) 117 | except KeyError as e: 118 | raise IHRException.STD_INVALID(traceback=True) 119 | except (ConnectionError, ValueError) as e: 120 | raise IHRException(str(e), traceback=True) 121 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.views.generic import TemplateView 3 | 4 | from rest_framework import permissions 5 | from drf_yasg.views import get_schema_view 6 | from drf_yasg import openapi 7 | 8 | 9 | from . import views 10 | #from .user_api import urls as user_urls 11 | 12 | #from rest_framework_swagger.views import get_swagger_view 13 | #schema_view = get_swagger_view(title='API') 14 | from drf_yasg.generators import OpenAPISchemaGenerator 15 | 16 | 17 | exposed_api = [ 18 | # User webpage not yet integrated 19 | #url(r'^user/sendregisteremail$', views.UserSendEmailView.as_view(), name='UserSendEmailListView'), 20 | #url(r'^user/sendforgetpasswordemail$', views.UserSendForgetPasswordEmailView.as_view(), name='UserSendForgetPasswordEmailListView'), 21 | #url(r'^user/register$', views.UserRegisterView.as_view(), name='UserRegisterListView'), 22 | # url(r'^user/login$', views.UserLoginView.as_view(), name='UserLoginListView'), 23 | # url(r'^user/logout$', views.UserLogoutView.as_view(), name='UserLogoutListView'), 24 | # url(r'^user/changepassword$', views.UserChangePasswordView.as_view(), name='UserChangePasswordListView'), 25 | # url(r'^user/forgetpassword$', views.UserForgetPasswordView.as_view(), name='UserForgetPasswordListView'), 26 | 27 | # url(r'^user/savechannel$', views.UserSaveChannelView.as_view(), name='UserSaveChannelListView'), 28 | # url(r'^user/getchannel$', views.UserGetChannelView.as_view(), name='UserGetChannelListView'), 29 | 30 | url(r'^ihr/api/networks/$', views.NetworkView.as_view(), name='networkListView'), 31 | url(r'^ihr/api/countries/$', views.CountryView.as_view(), name='countryListView'), 32 | url(r'^ihr/api/link/delay/$', views.DelayView.as_view(), name='delayListView'), 33 | url(r'^ihr/api/link/forwarding/$', views.ForwardingView.as_view(), name='forwardingListView'), 34 | url(r'^ihr/api/link/delay/alarms/$', views.DelayAlarmsView.as_view(), name='delayAlarmsListView'), 35 | url(r'^ihr/api/link/forwarding/alarms/$', views.ForwardingAlarmsView.as_view(), name='forwardingAlarmsListView'), 36 | url(r'^ihr/api/disco/events/$', views.DiscoEventsView.as_view(), name='discoEventsListView'), 37 | url(r'^ihr/api/hegemony/$', views.HegemonyView.as_view(), name='hegemonyListView'), 38 | url(r'^ihr/api/hegemony/alarms/$', views.HegemonyAlarmsView.as_view(), name='hegemonyAlarmsListView'), 39 | url(r'^ihr/api/hegemony/countries/$', views.HegemonyCountryView.as_view(), name='hegemonyCountryListView'), 40 | url(r'^ihr/api/hegemony/prefixes/$', views.HegemonyPrefixView.as_view(), name='hegemonyPrefixListView'), 41 | url(r'^ihr/api/hegemony/cones/$', views.HegemonyConeView.as_view(), name='hegemonyConeListView'), 42 | url(r'^ihr/api/network_delay/$', views.NetworkDelayView.as_view(), name='networkDelayListView'), 43 | url(r'^ihr/api/network_delay/locations/$', views.NetworkDelayLocationsView.as_view(), name='networkDelayLocationsListView'), 44 | url(r'^ihr/api/network_delay/alarms/$', views.NetworkDelayAlarmsView.as_view(), name='networkDelayAlarmsListView'), 45 | url(r'^ihr/api/metis/atlas/selection/$', views.MetisAtlasSelectionView.as_view(), name='metisAtlasSelectionListView'), 46 | url(r'^ihr/api/metis/atlas/deployment/$', views.MetisAtlasDeploymentView.as_view(), name='metisAtlasDeploymentListView'), 47 | url(r'^ihr/api/tr_hegemony/$', views.TRHegemonyView.as_view(), name='trHegemonyListView'), 48 | ] 49 | 50 | schema_view = get_schema_view( 51 | openapi.Info( 52 | title="IHR API", 53 | default_version='', 54 | description="""This RESTful API is intended for developers and researchers integrating IHR data to their workflow. API data is also available via our Python library. 55 | For bulk downloads please use: https://ihr-archive.iijlab.net/ 56 |
Parameters ending with __lte and __gte (acronyms for 'less than or equal to', and, 'greater than or equal to') are used for selecting a range of values.""", 57 | # terms_of_service="https://ihr.iijlab.net/ihr/en-us/documentation#Data_policy", 58 | contact=openapi.Contact(email="admin@ihr.live"), 59 | license=openapi.License(name="Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)", 60 | url="https://creativecommons.org/licenses/by-nc-sa/4.0/"), 61 | ), 62 | public=True, 63 | permission_classes=(permissions.AllowAny,), 64 | patterns=exposed_api, 65 | ) 66 | 67 | app_name = 'ihr' 68 | urlpatterns = [ 69 | url(r'^ihr/api/swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), 70 | # refered in the base.html template 71 | # User webpage not yet integrated 72 | #*user_urls.urlpatterns 73 | ] + exposed_api 74 | -------------------------------------------------------------------------------- /static/ihr/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ihr-django 2 | Internet Health Report API 3 | 4 | This is the implementation for the IHR API: https://ihr.iijlab.net/ihr/en-us/api 5 | 6 | 7 | # 📝 Table of Contents 8 | 9 | - [Getting started](#install-all) 10 | - [Setup without docker](#machine) 11 | - [Setup using docker (recommended)](#docker) 12 | - [Connecting to an existing postgres server](#docker-psql) 13 | - [Create initial database](#initial-db) 14 | - [Add test data to the database](#add-test-data) 15 | 16 | 17 | ## Getting started 18 | 19 | Required packages for Ubuntu: 20 | ```zsh 21 | sudo apt install apache2 python3 python3-pip postgresql postgresql-contrib 22 | ``` 23 | 24 | Install virtualenv and django: 25 | ```zsh 26 | pip3 install virtualenv 27 | virtualenv ihr 28 | ``` 29 | 30 | Install django: 31 | ```zsh 32 | cd ihr 33 | . bin/activate 34 | pip install django==2.2.27 35 | ``` 36 | 37 | Create a new django project: 38 | ```zsh 39 | django-admin startproject internetHealthReport 40 | ``` 41 | 42 | Copy IHR's django application : 43 | ```zsh 44 | cd internetHealthReport 45 | git clone git@github.com:InternetHealthReport/ihr-django.git ihr 46 | 47 | ``` 48 | 49 | Then copy settings.py, urls.py, wsgi.py, Dockerfile, docker compose and environment file to the correct place: 50 | ```zsh 51 | cp ihr/config/*.py internetHealthReport/ 52 | cp ihr/config/gunicorn_config.py ./ 53 | cp ihr/config/Dockerfile . 54 | cp ihr/config/docker-compose.yml . 55 | cp ihr/config/.env . 56 | ``` 57 | You may have to adjust some variables in .env (or settings.py if you don't use docker) to match your database, smtp account, recapcha credentials. 58 | 59 | ## Setup without docker 60 | 61 | install dependencies 62 | ```zsh 63 | pip install -r ihr/requirements.txt 64 | ``` 65 | 66 | make sure that the host of database in the settings is localhost 67 | 68 | ```zsh 69 | cd internetHealthReport 70 | nano settings.py 71 | ``` 72 | replace all `os.environ.get` instances (see .env for default values). 73 | 74 | [Create initial database.](#initial-db) 75 | 76 | Start django: 77 | ```zsh 78 | ./manage.py runserver 79 | ``` 80 | 81 | ## Setup with docker 82 | 83 | 84 | install docker 85 | 86 | ```zsh 87 | sudo apt install docker 88 | ``` 89 | 90 | 91 | make sure that the host of database in the settings is db 92 | 93 | ```zsh 94 | nano .env 95 | ``` 96 | update variables to match your database settings 97 | 98 | 99 | ## Connecting to an existing postgres server 100 | 101 | Get your local ip address 102 | 103 | ```zsh 104 | hostname -I 105 | ``` 106 | you will get something like that 107 | 108 | ```zsh 109 | xxx.xxx.x.xx 110 | ``` 111 | copy the first IP address and paste it in the .env file. 112 | Also you should allow your postgres to accept connections from outside: 113 | 114 | ```zsh 115 | sudo nano /etc/postgresql/**/main/postgresql.conf 116 | ``` 117 | 118 | change the following line 119 | 120 | ```zsh 121 | #listen_addresses = 'localhost' 122 | ``` 123 | 124 | to 125 | 126 | ```zsh 127 | listen_addresses = '*' 128 | ``` 129 | 130 | allow your postgres to accept connections from outside 131 | 132 | ```zsh 133 | sudo nano /etc/postgresql/**/main/pg_hba.conf 134 | ``` 135 | 136 | add the following line 137 | 138 | ```zsh 139 | host all all 172.xx.0.00/16 md5 140 | ``` 141 | xx may vary depending on postgres version but in newer versions it is 20 else it could be 17 142 | 143 | [Create initial database.](#initial-db) 144 | 145 | Start the docker container 146 | 147 | ```zsh 148 | docker compose up 149 | ``` 150 | And of course you need to have a postgres database running on your machine with the database and user specified in the .env file. 151 | 152 | # Initialize the database 153 | 154 | Create the database and django user (change password as needed): 155 | ```zsh 156 | sudo su postgres 157 | psql 158 | postgres=# CREATE DATABASE ihr; 159 | CREATE DATABASE 160 | postgres=# CREATE USER django WITH PASSWORD '123password456'; 161 | CREATE ROLE 162 | postgres=# ALTER ROLE django SET client_encoding TO 'utf8'; 163 | ALTER ROLE 164 | postgres=# ALTER ROLE django SET timezone TO 'UTC'; 165 | ALTER ROLE 166 | postgres=# GRANT ALL PRIVILEGES ON DATABASE ihr TO django; 167 | GRANT 168 | postgres=#\q 169 | exit 170 | ``` 171 | 172 | Create tables: 173 | ```zsh 174 | ./manage.py makemigrations ihr 175 | ./manage.py migrate 176 | ``` 177 | 178 | 179 | ## Add test data to the database 180 | In the production database some of the ids are changed to BIGINT. We should 181 | locally apply these changes before importing data: 182 | ```zsh 183 | psql -U django -d ihr -c "ALTER TABLE ihr_hegemony ALTER COLUMN id SET DATA TYPE bigint" 184 | psql -U django -d ihr -c "ALTER TABLE ihr_hegemony_prefix ALTER COLUMN id SET DATA TYPE bigint" 185 | psql -U django -d ihr -c "ALTER TABLE ihr_hegemony_country ALTER COLUMN id SET DATA TYPE bigint" 186 | psql -U django -d ihr -c "ALTER TABLE ihr_atlas_delay ALTER COLUMN id SET DATA TYPE bigint" 187 | ``` 188 | 189 | Download a database snapshot and load it: 190 | ```zsh 191 | wget https://ihr-archive.iijlab.net/ihr-dev/psql-snapshot/2022-03-10/2022-03-10_psql_snapshot.sql.lz4 192 | lz4 2022-03-10_psql_snapshot.sql.lz4 193 | psql -U django ihr < 2022-03-10_psql_snapshot.sql 194 | ``` 195 | 196 | ## Running the application 197 | Activate the python environment and lunch django server: 198 | ```zsh 199 | cd ihr 200 | . bin/activate 201 | internetHealthReport/manage.py runserver 202 | ``` 203 | Go to http://127.0.0.1:8000/hegemony/ to check if it is working. 204 | 205 | ## Working with a local instance of IHR website (https://github.com/InternetHealthReport/ihr-website) 206 | To redirect all API calls to the local django server you should change the API 207 | URL in ihr-website/src/plugins/IhrApi.js: 208 | ```js 209 | const IHR_API_BASE = 'http://127.0.0.1:8000/' 210 | ``` 211 | 212 | -------------------------------------------------------------------------------- /config/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from .test_setup import TestSetUp 2 | from ihr.const import ConfirmationEmail, ChangePasswordEmail, StrErrors, Msg, POOL, std_response 3 | import redis 4 | conn = redis.Redis(connection_pool=POOL) 5 | from rest_framework.authtoken.models import Token 6 | 7 | 8 | class TestViews(TestSetUp): 9 | 10 | def test_usersendemail(self): 11 | res=self.client.post(self.sendemail_url, self.email_data, format="json") 12 | code = conn.get(f"Confirmation_{self.email_data['email']}") 13 | self.assertEqual(res.status_code,200) 14 | self.assertIsNotNone(code) 15 | self.assertEqual(res.json()["msg"], Msg.CODE_SENT) 16 | 17 | def test_user_cannot_register_with_invalid_data(self): 18 | res=self.client.post(self.register_url) 19 | self.assertEqual(res.status_code,400) 20 | 21 | def test_user_can_register(self): 22 | res=self.client.post(self.register_url, self.user_data, format="json") 23 | self.assertEqual(res.status_code,201) 24 | self.assertEqual(res.json()["msg"], Msg.REGISTER_SUCCEEDED) 25 | 26 | def test_user_cannot_register_with_existing_email(self): 27 | self.client.post(self.register_url, self.user_data, format="json") 28 | res=self.client.post(self.register_url, self.user_data, format="json") 29 | self.assertEqual(res.status_code,200) 30 | self.assertEqual(res.json()["msg"], Msg.USER_ALREADY_REGISTERED) 31 | 32 | def test_user_cannot_register_with_invalid_code(self): 33 | self.user_data["code"] = "123456" 34 | res=self.client.post(self.register_url, self.user_data, format="json") 35 | self.assertEqual(res.status_code,202) 36 | self.assertEqual(res.json()["msg"], Msg.CODE_ERROR) 37 | 38 | 39 | def test_user_cannot_confirm_with_invalid_data(self): 40 | res=self.client.post(self.register_url) 41 | self.assertEqual(res.status_code,400) 42 | 43 | 44 | def test_user_cannot_login_with_invalid_data(self): 45 | res=self.client.post(self.login_url) 46 | self.assertEqual(res.status_code,401) 47 | 48 | def test_user_can_login(self): 49 | self.client.post(self.register_url, self.user_data, format="json") 50 | res=self.client.post(self.login_url, self.user_data, format="json") 51 | self.assertEqual(res.status_code,200) 52 | self.assertEqual(res.json()["msg"], Msg.LOGIN_SUCCEEDED) 53 | 54 | def test_user_cannot_login_with_unregistered_email(self): 55 | self.user_data["email"] = "wrongemail@email.com" 56 | res = self.client.post(self.login_url, self.user_data, format="json") 57 | self.assertEqual(res.status_code,202) 58 | self.assertEqual(res.json()["msg"], Msg.USER_NOT_EXIST) 59 | 60 | def test_user_can_logout(self): 61 | self.client.post(self.register_url, self.user_data, format="json") 62 | login_res = self.client.post(self.login_url, self.user_data, format="json") 63 | token = login_res.json()["token"] 64 | 65 | headers = {"HTTP_AUTHORIZATION":f'{token}'} 66 | logout_res = self.client.post(self.logout_url, **headers) 67 | 68 | self.assertEqual(logout_res.status_code, 202) 69 | self.assertEqual(logout_res.json()["msg"], Msg.LOGOUT_SUCCEEDED) 70 | 71 | def test_user_cannot_change_password_with_invalid_data(self): 72 | res=self.client.post(self.change_password_url) 73 | self.assertEqual(res.status_code,400) 74 | 75 | def test_user_can_change_password(self): 76 | self.client.post(self.register_url, self.user_data, format="json") 77 | change_password_res = self.client.post(self.change_password_url, self.change_password_data, format="json") 78 | self.assertEqual(change_password_res.status_code, 200) 79 | self.assertEqual(change_password_res.json()["msg"], Msg.CHANGE_PASSWORD_SUCCEEDED) 80 | 81 | def test_user_cannot_change_password_with_invalid_password(self): 82 | self.client.post(self.register_url, self.user_data, format="json") 83 | self.change_password_data["password"] = "wrongpassword123" 84 | change_password_res = self.client.post(self.change_password_url, self.change_password_data, format="json") 85 | self.assertEqual(change_password_res.status_code, 202) 86 | self.assertEqual(change_password_res.json()["msg"], Msg.LOGIN_FAILED) 87 | 88 | def test_user_cannot_change_password_with_invalid_email(self): 89 | self.client.post(self.register_url, self.user_data, format="json") 90 | self.change_password_data["email"] = "invalid@email.com" 91 | change_password_res = self.client.post(self.change_password_url, self.change_password_data, format="json") 92 | self.assertEqual(change_password_res.status_code, 202) 93 | self.assertEqual(change_password_res.json()["msg"], Msg.USER_NOT_EXIST) 94 | 95 | def test_user_send_forget_password_email(self): 96 | res=self.client.post(self.sendforgetpasswordemail_url, self.email_data, format="json") 97 | code = conn.get(f"ChangePassword_{self.email_data['email']}") 98 | self.assertEqual(res.status_code,200) 99 | self.assertIsNotNone(code) 100 | self.assertEqual(res.json()["msg"], Msg.CODE_SENT) 101 | 102 | def test_user_forget_password_email(self): 103 | self.client.post(self.register_url, self.user_data, format="json") 104 | 105 | forget_password_res = self.client.post(self.forget_password_url, self.forget_password_data, format="json") 106 | self.assertEqual(forget_password_res.status_code, 200) 107 | self.assertEqual(forget_password_res.json()["msg"], Msg.CHANGE_PASSWORD_SUCCEEDED) 108 | 109 | def test_user_cannot_forget_password_with_invalid_email(self): 110 | self.client.post(self.register_url, self.user_data, format="json") 111 | self.forget_password_data["email"] = "invalid@email.com" 112 | self.forget_password_data["code"] = "123456" 113 | conn.set(f"ChangePassword_{self.forget_password_data['email']}", self.forget_password_data["code"]) 114 | res = self.client.post(self.forget_password_url, self.forget_password_data, format="json") 115 | self.assertEqual(res.status_code, 202) 116 | self.assertEqual(res.json()["msg"], Msg.USER_NOT_EXIST) 117 | 118 | def test_user_cannot_forget_password_with_invalid_code(self): 119 | self.client.post(self.register_url, self.user_data, format="json") 120 | self.forget_password_data["code"] = "123456" 121 | res = self.client.post(self.forget_password_url, self.forget_password_data, format="json") 122 | self.assertEqual(res.status_code, 202) 123 | self.assertEqual(res.json()["msg"], Msg.CODE_ERROR) 124 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for internetHealthReport project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = os.environ.get("SECRET_KEY") 24 | RECAPTCHA_SECRET = os.environ.get("RECAPTCHA_SECRET") 25 | EMAIL_HOST_PASSWORD = os.environ['EMAIL_HOST_PASSWORD'] 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = bool(os.environ.get("DEBUG")) 29 | 30 | ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") 31 | 32 | SITE_ID = os.environ.get("SITE_ID") 33 | 34 | USE_X_FORWARDED_HOST = True 35 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'ihr.apps.IHRConfig', 41 | 'django.contrib.admin', 42 | 'django.contrib.auth', 43 | 'django.contrib.contenttypes', 44 | 'django.contrib.sessions', 45 | 'django.contrib.messages', 46 | 'django.contrib.staticfiles', 47 | 'django.contrib.flatpages', 48 | 'django.contrib.sites', 49 | 'rest_framework', 50 | 'rest_framework.authtoken', 51 | 'crispy_forms', 52 | 'django_filters', 53 | 'django_extensions', 54 | 'corsheaders', 55 | 'drf_yasg' 56 | ] 57 | 58 | MIDDLEWARE = [ 59 | 'django.middleware.security.SecurityMiddleware', 60 | 'django.contrib.sessions.middleware.SessionMiddleware', 61 | 'corsheaders.middleware.CorsMiddleware', 62 | 'django.middleware.cache.UpdateCacheMiddleware', 63 | 'django.middleware.common.CommonMiddleware', 64 | 'django.middleware.csrf.CsrfViewMiddleware', 65 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 66 | 'django.middleware.cache.CacheMiddleware', 67 | # 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 68 | 'django.contrib.messages.middleware.MessageMiddleware', 69 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 70 | 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 71 | 'django.middleware.cache.FetchFromCacheMiddleware', 72 | ] 73 | 74 | CACHE_MIDDLEWARE_ALIAS = 'default' 75 | CACHE_MIDDLEWARE_SECONDS = 1800 76 | CACHE_MIDDLEWARE_KEY_PREFIX = 'ihr-backend.iijlab.net' 77 | 78 | CORS_ORIGIN_ALLOW_ALL = True 79 | 80 | ROOT_URLCONF = 'internetHealthReport.urls' 81 | 82 | TEMPLATES = [ 83 | { 84 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 85 | 'DIRS': ["templates", "/var/www/html/ihr"], 86 | 'APP_DIRS': True, 87 | 'OPTIONS': { 88 | 'context_processors': [ 89 | 'django.template.context_processors.debug', 90 | 'django.template.context_processors.request', 91 | 'django.contrib.auth.context_processors.auth', 92 | 'django.contrib.messages.context_processors.messages', 93 | ], 94 | }, 95 | }, 96 | ] 97 | 98 | WSGI_APPLICATION = 'internetHealthReport.wsgi.application' 99 | 100 | REST_FRAMEWORK = { 101 | 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticatedOrReadOnly',), 102 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 103 | 'PAGE_SIZE': 100000, 104 | # 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) 105 | 'DEFAULT_FILTER_BACKENDS': ('ihr.apps.NoMarkupDjangoFilterBackend', 'rest_framework.filters.OrderingFilter',), 106 | 'HTML_SELECT_CUTOFF': 100, 107 | 'DEFAULT_THROTTLE_CLASSES': ( 108 | 'rest_framework.throttling.AnonRateThrottle', 109 | 'rest_framework.throttling.UserRateThrottle' 110 | ), 111 | 'DEFAULT_THROTTLE_RATES': { 112 | 'anon': '250000/day', 113 | 'user': '1000000/day' 114 | }, 115 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 116 | 'rest_framework.authentication.TokenAuthentication', 117 | ), 118 | 'DEFAULT_PERMISSION_CLASSES': ( 119 | 'rest_framework.permissions.AllowAny', 120 | ), 121 | 'EXCEPTION_HANDLER': 'ihr.user_api.utility.IHR_exception_handler', 122 | 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' 123 | } 124 | 125 | # CACHES = { 126 | # 'default': { 127 | # 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 128 | # 'LOCATION': '127.0.0.1:11211', 129 | # 'TIMEOUT': 3600 130 | # }, 131 | # } 132 | 133 | # Database 134 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 135 | 136 | DATABASES = { 137 | 'default': { 138 | # 'ENGINE': 'django.db.backends.sqlite3', 139 | # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 140 | 'ENGINE': 'django.db.backends.postgresql', 141 | 'NAME': os.environ.get("DB_NAME"), 142 | 'USER': os.environ.get("DB_USER"), 143 | 'PASSWORD': os.environ.get("DB_PASSWORD"), 144 | 'HOST': os.environ.get("DB_HOST"), 145 | 'PORT': os.environ.get("DB_PORT") 146 | } 147 | } 148 | 149 | 150 | # Password validation 151 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 152 | 153 | AUTH_PASSWORD_VALIDATORS = [ 154 | { 155 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 156 | }, 157 | { 158 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 159 | }, 160 | { 161 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 162 | }, 163 | { 164 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 165 | }, 166 | ] 167 | 168 | PASSWORD_HASHERS = [ 169 | 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 170 | 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 171 | 'django.contrib.auth.hashers.Argon2PasswordHasher', 172 | 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 173 | ] 174 | 175 | # Internationalization 176 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 177 | 178 | LANGUAGE_CODE = 'en-us' 179 | 180 | TIME_ZONE = 'UTC' 181 | 182 | USE_I18N = True 183 | 184 | USE_L10N = True 185 | 186 | USE_TZ = True 187 | 188 | 189 | # Static files (CSS, JavaScript, Images) 190 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 191 | 192 | STATIC_URL = '/static/' 193 | STATIC_ROOT = '/home/romain/ihr/internetHealthReport/ihr/static/' 194 | 195 | EMAIL_USE_TLS = True 196 | EMAIL_HOST = os.environ.get("EMAIL_HOST") 197 | EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") 198 | EMAIL_PORT = 587 199 | 200 | ADMINS = ( 201 | ('IHR administrators', 'admin@ihr.live'), # email will be sent this email 202 | ) 203 | 204 | MANAGERS = ADMINS 205 | 206 | AUTH_USER_MODEL = 'ihr.IHRUser' 207 | 208 | 209 | LOGGING = { 210 | 'version': 1, 211 | 'disable_existing_loggers': False, 212 | 'handlers': { 213 | 'file': { 214 | 'level': 'INFO', 215 | 'class': 'logging.FileHandler', 216 | 'filename': 'django-info.log', 217 | }, 218 | }, 219 | 'loggers': { 220 | 'django': { 221 | 'handlers': ['file'], 222 | 'level': 'INFO', 223 | 'propagate': True, 224 | }, 225 | }, 226 | } 227 | 228 | -------------------------------------------------------------------------------- /templates/ihr/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | {% block title %} Internet Health Report - Homepage {% endblock %} 8 | 9 | {% block errorMessage %}{% endblock %} 10 | 11 | {% block body %} 12 |
13 |

Delay changes for tier 1 networks

14 |
15 | 18 |
19 |
20 |
21 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 |

Network disconnections

54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |

Monitored networks

64 |
65 | 68 |
69 | 70 |
71 |
72 | 79 |
80 |
81 | 89 |
90 |
91 |
92 |
93 |
    94 | {% for cc in monitoredCountry %} 95 | {% if forloop.counter < 16 %} 96 |
  • {{ cc.name }}
  • 97 | {% endif %} 98 | {% endfor %} 99 |
100 |
101 |
102 | 110 |
111 |
112 |
113 |
114 | 217 | {% endblock %} 218 | -------------------------------------------------------------------------------- /templates/ihr/country_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | 8 | {% block body %} 9 |
10 |
11 |

{{ country.name}}

12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 26 |
27 |
28 |
29 |
30 |
31 | 32 | {% if country.disco %} 33 |
34 |
35 |

Network Disconnections

36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 |
Click on the graph for more details.
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 | {% endif %} 66 | 67 | 68 | 69 | 300 | 301 | 302 | 303 | {% endblock %} 304 | -------------------------------------------------------------------------------- /templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% load staticfiles %} 5 | {% load rest_framework %} 6 | {% load i18n %} 7 | 8 | {% block head %} 9 | 10 | {% block meta %} 11 | 12 | 13 | {% endblock %} 14 | 15 | {% block style %} 16 | 17 | 18 | 19 | {% endblock %} 20 | 21 | {% endblock %} 22 | 23 | {% block body %} 24 |
25 | 26 |
27 | {% block breadcrumbs %} 28 | 37 | {% endblock %} 38 | 39 | 40 |
41 | 42 | {% if 'GET' in allowed_methods %} 43 |
44 |
45 | {% if api_settings.URL_FORMAT_OVERRIDE %} 46 |
47 | GET 48 | 49 | 52 | 59 |
60 | {% else %} 61 | GET 62 | {% endif %} 63 |
64 |
65 | {% endif %} 66 | 67 | {% if options_form %} 68 |
69 | 70 |
71 | {% endif %} 72 | 73 | {% if delete_form %} 74 | 75 | 76 | 77 | 92 | {% endif %} 93 | 94 | {% if filter_form %} 95 | 99 | {% endif %} 100 | 101 |
102 | 105 |
106 | {% block description %} 107 | {{ description }} 108 | {% endblock %} 109 |
110 | 111 | {% if paginator %} 112 | 115 | {% endif %} 116 | 117 |
118 |
{{ request.method }} {{ request.get_full_path }}
119 |
120 | 121 |
122 |
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}
123 | {% for key, val in response_headers.items %}{{ key }}: {{ val|break_long_headers }}
124 | {% endfor %}
125 | {{ content }}
{% endautoescape %} 126 |
127 |
128 | 129 | {% if display_edit_forms %} 130 | 131 | {% if post_form or raw_data_post_form %} 132 |
133 | {% if post_form %} 134 | 142 | {% endif %} 143 | 144 |
145 | {% if post_form %} 146 |
147 | {% with form=post_form %} 148 |
149 |
150 | {% csrf_token %} 151 | {{ post_form }} 152 |
153 | 154 |
155 |
156 |
157 | {% endwith %} 158 |
159 | {% endif %} 160 | 161 |
162 | {% with form=raw_data_post_form %} 163 |
164 |
165 | {% include "rest_framework/raw_data_form.html" %} 166 |
167 | 168 |
169 |
170 |
171 | {% endwith %} 172 |
173 |
174 |
175 | {% endif %} 176 | 177 | {% if put_form or raw_data_put_form or raw_data_patch_form %} 178 |
179 | {% if put_form %} 180 | 188 | {% endif %} 189 | 190 |
191 | {% if put_form %} 192 |
193 |
194 |
195 | {{ put_form }} 196 |
197 | 198 |
199 |
200 |
201 |
202 | {% endif %} 203 | 204 |
205 | {% with form=raw_data_put_or_patch_form %} 206 |
207 |
208 | {% include "rest_framework/raw_data_form.html" %} 209 |
210 | {% if raw_data_put_form %} 211 | 212 | {% endif %} 213 | {% if raw_data_patch_form %} 214 | 215 | {% endif %} 216 |
217 |
218 |
219 | {% endwith %} 220 |
221 |
222 |
223 | {% endif %} 224 | {% endif %} 225 |
226 |
227 |
228 | 229 | {% endblock %} 230 | 231 | {% block script %} 232 | 237 | 238 | 239 | 240 | 241 | 242 | 247 | 248 | {% if filter_form %} 249 | {{ filter_form }} 250 | {% endif %} 251 | 252 | {% endblock %} 253 | 254 | -------------------------------------------------------------------------------- /user_api/views.py: -------------------------------------------------------------------------------- 1 | from .const import ConfirmationEmail, ResetPasswordEmail, StrErrors 2 | from .utility import account_activation_token, google_token_verification, parse_request, std_response, EmailChangeTokenGenerator, IHRException 3 | from .serializers import IHRUserSerializer 4 | from ..models import IHRUser, MonitoredASN, ASN, UserManager, EmailChangeRequest 5 | from django.shortcuts import render 6 | from rest_framework import viewsets 7 | from rest_framework.decorators import action 8 | from rest_framework.response import Response 9 | from rest_framework.status import ( 10 | HTTP_400_BAD_REQUEST, 11 | HTTP_401_UNAUTHORIZED, 12 | HTTP_403_FORBIDDEN, 13 | HTTP_404_NOT_FOUND, 14 | HTTP_410_GONE, 15 | HTTP_409_CONFLICT, 16 | HTTP_200_OK 17 | ) 18 | from django.conf import settings as conf_settings 19 | 20 | from django.core.mail import send_mail 21 | from smtplib import SMTPException 22 | from email.errors import HeaderParseError 23 | import json 24 | from django.db import transaction, IntegrityError 25 | from datetime import datetime 26 | from rest_framework.authtoken.models import Token 27 | from rest_framework.permissions import IsAuthenticated, AllowAny 28 | from itertools import islice 29 | 30 | class UserView(viewsets.GenericViewSet): 31 | queryset = IHRUser.objects 32 | serializer_class = IHRUserSerializer 33 | 34 | @action(detail=False, methods=["POST"], permission_classes=[AllowAny]) 35 | def sign_in(self, request): 36 | content = parse_request(request) 37 | try: 38 | google_token_verification(content) 39 | with transaction.atomic(): 40 | user = self.get_queryset().create_user(content["email"], content["password"]) 41 | token = account_activation_token.make_token(user) 42 | confirmation_email = ConfirmationEmail(user.email, token) 43 | send_mail( 44 | 'Account activation', 45 | confirmation_email.PLAIN, 46 | 'noreplay@ihr.iij.jp', 47 | [user.email], 48 | fail_silently=False, 49 | ) 50 | except IntegrityError as e: 51 | return std_response(StrErrors.DUPLICATED, HTTP_409_CONFLICT) 52 | except (ValueError, SMTPException, HeaderParseError, KeyError) as e: 53 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST) 54 | return Response(status=HTTP_200_OK) 55 | 56 | @action(detail=False, methods=["POST"], permission_classes=[AllowAny]) 57 | def validate(self, request): 58 | content = parse_request(request) 59 | try: 60 | with transaction.atomic(): 61 | user = self.get_queryset().get(email=content["email"]) 62 | if not user.check_password(content["password"]): 63 | return std_response(StrErrors.WRONG_DATA, HTTP_403_FORBIDDEN) 64 | if user.is_active: 65 | return std_response(StrErrors.ALREADY_VALIDATED, HTTP_409_CONFLICT) 66 | if not account_activation_token.check_token(user, content["token"]): 67 | return std_response(StrErrors.WRONG_DATA, HTTP_403_FORBIDDEN) 68 | user.is_active = True 69 | user.save() 70 | except (KeyError, IHRUser.DoesNotExist) as e: 71 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST) 72 | return self.login(request) 73 | 74 | @action(detail=False, methods=['POST'], permission_classes=[IsAuthenticated]) 75 | def sign_out(self, request): 76 | request.user.delete() 77 | return Response(status=HTTP_200_OK) 78 | 79 | @action(detail=False, methods=["POST"], permission_classes=[AllowAny]) 80 | def login(self, request): 81 | content = parse_request(request) 82 | try: 83 | with transaction.atomic(): 84 | user = self.get_queryset().get(email=content["email"]) 85 | if not user.is_active and not user.check_password(content["password"]): 86 | return std_response(StrErrors.WRONG_DATA, HTTP_403_FORBIDDEN) 87 | user.last_login = datetime.utcnow() 88 | user.save() 89 | token,_ = Token.objects.get_or_create(user=user) 90 | except (KeyError, IHRUser.DoesNotExist) as e: 91 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST) 92 | return Response({'token': token.key}, status=HTTP_200_OK) 93 | 94 | @action(detail=False, methods=['POST'], permission_classes=[IsAuthenticated]) 95 | def logout(self, request): 96 | request.user.auth_token.delete() 97 | return Response(status=HTTP_200_OK) 98 | 99 | @action(detail=False, methods=["POST"], permission_classes=[AllowAny]) 100 | def request_reset_password(self, request): 101 | content = parse_request(request) 102 | try: 103 | user = self.get_queryset().get(email=content["email"]) 104 | google_token_verification(content) 105 | token = account_activation_token.make_token(user) 106 | reset_email = ResetPasswordEmail(user.email, token) 107 | send_mail( 108 | 'Account activation', 109 | reset_email.PLAIN, 110 | 'noreplay@ihr.iij.jp', 111 | [user.email], 112 | fail_silently=False, 113 | ) 114 | except IHRUser.DoesNotExist as e: 115 | return std_response(StrErrors.WRONG_DATA, HTTP_403_FORBIDDEN) 116 | except (ValueError, SMTPException, HeaderParseError, KeyError) as e: 117 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST) 118 | return Response(status=HTTP_200_OK) 119 | 120 | @action(detail=False, methods=["POST"], permission_classes=[AllowAny]) 121 | def reset_password(self, request): 122 | content = parse_request(request) 123 | try: 124 | with transaction.atomic(): 125 | user = self.get_queryset().get(email=content["email"]) 126 | if not account_activation_token.check_token(user, content["token"]): 127 | return std_response(StrErrors.WRONG_DATA, HTTP_403_FORBIDDEN) 128 | user.set_password(content["password"]) 129 | user.save() 130 | except (KeyError, IHRUser.DoesNotExist) as e: 131 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST) 132 | return self.login(request) 133 | 134 | @action(detail=False, methods=['GET'], permission_classes=[IsAuthenticated]) 135 | def verify_token(self, request): 136 | return Response(status=HTTP_200_OK) 137 | 138 | @action(detail=False, methods=["POST"], permission_classes=[]) 139 | def change_credentials(self, request): 140 | email = request.data.get("email") 141 | token = Token.objects.get(key=request.META.get("HTTP_AUTHORIZATION").split('=')[-1]) 142 | request.user = token.user 143 | if email is not None: 144 | #change email 145 | if self.get_queryset().filter(email=email).count() > 0: 146 | return std_response(StrErrors.DUPLICATED, HTTP_409_CONFLICT) 147 | try: 148 | change = EmailChangeRequest(user=request.user, new_email=email) 149 | change.save() 150 | token_checker = EmailChangeTokenGenerator(change.new_email, change.request_time) 151 | token = token_checker.make_token(request.user) 152 | reset_email = ConfirmationEmail(change.new_email, token, password_change=True) 153 | send_mail( 154 | 'Email change', 155 | reset_email.PLAIN, 156 | conf_settings.EMAIL_HOST_USER, 157 | [change.new_email], 158 | fail_silently=False, 159 | ) 160 | except (SMTPException, HeaderParseError) as e: 161 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST) 162 | 163 | password = request.data.get("password") 164 | if password is not None: 165 | #change password 166 | request.user.set_password(password) 167 | request.user.save() 168 | 169 | return Response(status=HTTP_200_OK) 170 | 171 | @action(detail=False, methods=["POST"], permission_classes=[AllowAny]) 172 | def change_email(self, request): 173 | content = parse_request(request) 174 | #import pdb; pdb.set_trace() 175 | try: 176 | change = EmailChangeRequest.objects.get(new_email=content["email"]) 177 | user = change.user 178 | if not user.check_password(content["password"]): 179 | return std_response(StrErrors.WRONG_DATA, HTTP_403_FORBIDDEN) 180 | 181 | now = datetime.utcnow() 182 | if (now - change.request_time.replace(tzinfo=None)).days * 24 * 60 > EmailChangeRequest.VALIDITY: 183 | raise IHRException(status_code=HTTP_410_GONE, message=StrErrors.WRONG_DATA, log=False) 184 | 185 | if self.get_queryset().filter(email=content["email"]).count() > 0: 186 | raise IHRException(status_code=HTTP_409_CONFLICT, message=StrErrors.DUPLICATED, log=False) 187 | 188 | token_checker = EmailChangeTokenGenerator(change.new_email, change.request_time) 189 | if token_checker.check_token(user, content["token"]): 190 | user.email = change.new_email 191 | user.save() 192 | change.delete() 193 | return self.login(request) 194 | except (KeyError, EmailChangeRequest.DoesNotExist) as e: 195 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST,) 196 | except IHRException as e: 197 | change.delete() 198 | raise e 199 | return std_response(StrErrors.WRONG_DATA, HTTP_400_BAD_REQUEST) 200 | 201 | @action(detail=False, methods=["POST"], permission_classes=[]) 202 | def show(self, request): 203 | token = Token.objects.get(key=request.META.get("HTTP_AUTHORIZATION").split('=')[-1]) 204 | serializer = self.get_serializer(token.user) 205 | return Response(serializer.data, status=HTTP_200_OK) 206 | 207 | @action(detail=False, methods=["POST"], permission_classes=[IsAuthenticated]) 208 | def add_monitoring(self, request): 209 | monitored_asn = request.data.get("monitoredasn") 210 | if not monitored_asn: 211 | return std_response(StrErrors.INPUT.ADD_MONITORING, HTTP_400_BAD_REQUEST) 212 | asn_list = [] 213 | for i, monitored in enumerate(monitored_asn): 214 | for j in range(i + 1, len(monitored_asn)): 215 | if monitored["asnumber"] == monitored_asn[j]["asnumber"]: 216 | return std_response(StrErrors.INPUT.DUPLICATED, HTTP_400_BAD_REQUEST) 217 | asn_list.append(monitored["asnumber"]) 218 | 219 | monitored_update, batch = [], [] 220 | try: 221 | already_monitored = MonitoredASN.objects.filter(user=request.user) 222 | already_monitored.all().exclude(asn__in=asn_list).delete() 223 | with transaction.atomic(): 224 | for monitored in monitored_asn: 225 | for i, am in enumerate(already_monitored): 226 | print("here", am) 227 | if am.asn.number == monitored["asnumber"]: 228 | am.notifylevel = monitored["notifylevel"] 229 | am.save() 230 | print(am.asn.number, "save") 231 | break 232 | else: #if it's not an update is a new monitor 233 | print(monitored["asnumber"], "bulk") 234 | batch.append(MonitoredASN(user=request.user, asn=ASN.objects.get(number=monitored["asnumber"]), notifylevel=monitored["notifylevel"])) 235 | 236 | MonitoredASN.objects.bulk_create(batch) 237 | except ASN.DoesNotExist as e: 238 | return std_response(StrErrors.ASN_DOESNOT_EXIST, HTTP_400_BAD_REQUEST) 239 | return Response(status=HTTP_200_OK) 240 | -------------------------------------------------------------------------------- /serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import ASN, Country, Delay, Forwarding, Delay_alarms, Forwarding_alarms, Disco_events, Disco_probes, Hegemony, HegemonyCone, Atlas_location, Atlas_delay, Atlas_delay_alarms, Hegemony_alarms, Hegemony_country, Hegemony_prefix, Metis_atlas_selection, Metis_atlas_deployment, TR_hegemony 3 | 4 | class UserRegisterSerializer(serializers.Serializer): 5 | email = serializers.EmailField(required=True) 6 | password = serializers.CharField(required=True) 7 | code = serializers.CharField(required=True) 8 | 9 | def validate(self, data): 10 | return data 11 | 12 | 13 | class UserSerializer(serializers.Serializer): 14 | email = serializers.EmailField(required=True) 15 | password = serializers.CharField(required=True) 16 | 17 | def validate(self, data): 18 | return data 19 | 20 | class UserEmailSerializer(serializers.Serializer): 21 | email = serializers.EmailField(required=True) 22 | 23 | def validate(self, data): 24 | return data 25 | 26 | class UserLoginSerializer(serializers.Serializer): 27 | email = serializers.EmailField(required=True) 28 | password = serializers.CharField(required=True) 29 | 30 | def validate(self, data): 31 | return data 32 | 33 | class UserChangePasswordSerializer(serializers.Serializer): 34 | email = serializers.EmailField(required=True) 35 | password = serializers.CharField(required=True) 36 | new_password = serializers.CharField(required=True) 37 | 38 | def validate(self, data): 39 | return data 40 | 41 | class UserForgetPasswordSerializer(serializers.Serializer): 42 | email = serializers.EmailField(required=True) 43 | new_password = serializers.CharField(required=True) 44 | code = serializers.CharField(required=True) 45 | 46 | def validate(self, data): 47 | return data 48 | 49 | class DelaySerializer(serializers.ModelSerializer): 50 | queryset = Delay.objects.select_related("asn") 51 | asn_name = serializers.PrimaryKeyRelatedField( 52 | queryset=queryset, source='asn.name', 53 | help_text="Name of the Autonomous System corresponding to the reported IP address.") 54 | magnitude = serializers.FloatField(help_text="Amplitude of the delay change") 55 | 56 | class Meta: 57 | model = Delay 58 | fields = ('asn', 'timebin', 'magnitude', 'asn_name') 59 | 60 | class DelayAlarmsSerializer(serializers.ModelSerializer): 61 | queryset = Delay_alarms.objects.prefetch_related('msmid', "asn").all() 62 | msmid = serializers.StringRelatedField(many=True) 63 | asn_name = serializers.PrimaryKeyRelatedField( 64 | queryset=queryset, source='asn.name', 65 | help_text="Name of the Autonomous System corresponding to the reported IP address.") 66 | 67 | class Meta: 68 | model = Delay_alarms 69 | fields = ('asn', 70 | 'asn_name', 71 | 'timebin', 72 | 'link', 73 | 'medianrtt', 74 | 'diffmedian', 75 | 'deviation', 76 | 'nbprobes', 77 | 'msm_prb_ids', 78 | 'msmid') 79 | 80 | class ForwardingSerializer(serializers.ModelSerializer): 81 | queryset = Forwarding.objects.select_related("asn") 82 | asn_name = serializers.PrimaryKeyRelatedField( 83 | queryset=queryset, source='asn.name', 84 | help_text="Name of the Autonomous System corresponding to the reported IP address.") 85 | 86 | class Meta: 87 | model = Forwarding 88 | fields = ('asn', 'timebin', 'magnitude', 'asn_name') 89 | 90 | class ForwardingAlarmsSerializer(serializers.ModelSerializer): 91 | queryset = Forwarding_alarms.objects.prefetch_related('msmid', 'asn').all() 92 | msmid = serializers.StringRelatedField(many=True) 93 | asn_name = serializers.PrimaryKeyRelatedField( 94 | queryset=queryset, source='asn.name', 95 | help_text="Name of the Autonomous System corresponding to the reported IP address.") 96 | 97 | class Meta: 98 | model = Forwarding_alarms 99 | fields = ('asn', 100 | 'asn_name', 101 | 'timebin', 102 | 'ip', 103 | 'correlation', 104 | 'pktdiff', 105 | 'previoushop', 106 | 'responsibility', 107 | 'msm_prb_ids', 108 | 'msmid') 109 | 110 | class DiscoProbesSerializer(serializers.ModelSerializer): 111 | class Meta: 112 | model = Disco_probes 113 | fields = ('probe_id', 114 | 'ipv4', 115 | 'prefixv4', 116 | 'event', 117 | 'starttime', 118 | 'endtime', 119 | 'level', 120 | 'lat', 121 | 'lon') 122 | 123 | class DiscoEventsSerializer(serializers.ModelSerializer): 124 | discoprobes = DiscoProbesSerializer(many=True, read_only=True) 125 | 126 | class Meta: 127 | model = Disco_events 128 | fields = ('id', 129 | 'streamtype', 130 | 'streamname', 131 | 'starttime', 132 | 'endtime', 133 | 'avglevel', 134 | 'nbdiscoprobes', 135 | 'totalprobes', 136 | 'ongoing', 137 | 'discoprobes') 138 | 139 | 140 | class HegemonySerializer(serializers.ModelSerializer): 141 | asn_name = serializers.CharField(source='asn.name', 142 | help_text="Autonomous System name of the dependency.") 143 | originasn_name = serializers.CharField( 144 | source='originasn.name', help_text="Autonomous System name of the dependent network.") 145 | 146 | class Meta: 147 | model = Hegemony 148 | fields = ('timebin', 149 | 'originasn', 150 | 'asn', 151 | 'hege', 152 | 'af', 153 | 'asn_name', 154 | 'originasn_name') 155 | 156 | class HegemonyAlarmsSerializer(serializers.ModelSerializer): 157 | queryset = Hegemony_alarms.objects.prefetch_related("asn","originasn").all() 158 | asn_name = serializers.CharField(source='asn.name', 159 | help_text="Autonomous System name of the reported dependency.") 160 | originasn_name = serializers.CharField(source='originasn.name', 161 | help_text="Autonomous System name of the reported dependent network.") 162 | 163 | class Meta: 164 | model = Hegemony_alarms 165 | fields = ('timebin', 166 | 'originasn', 167 | 'asn', 168 | 'deviation', 169 | 'af', 170 | 'asn_name', 171 | 'originasn_name') 172 | 173 | 174 | class HegemonyConeSerializer(serializers.ModelSerializer): 175 | class Meta: 176 | model = HegemonyCone 177 | fields = ('timebin', 'asn', 'conesize', 'af') 178 | 179 | class HegemonyCountrySerializer(serializers.ModelSerializer): 180 | asn_name = serializers.CharField(source='asn.name', 181 | help_text="Autonomous System name of the dependency.") 182 | 183 | class Meta: 184 | model = Hegemony_country 185 | fields = ('timebin', 186 | 'country', 187 | 'asn', 188 | 'hege', 189 | 'af', 190 | 'asn_name', 191 | 'weight', 192 | 'weightscheme', 193 | 'transitonly') 194 | 195 | class HegemonyPrefixSerializer(serializers.ModelSerializer): 196 | originasn_name = serializers.CharField(source='originasn.name', 197 | help_text="Autonomous System name of the ASN originating the prefix.") 198 | asn_name = serializers.CharField(source='asn.name', 199 | help_text="Autonomous System name of the dependency.") 200 | 201 | class Meta: 202 | model = Hegemony_prefix 203 | fields = ('timebin', 204 | 'prefix', 205 | 'originasn', 206 | 'country', 207 | 'asn', 208 | 'hege', 209 | 'af', 210 | 'visibility', 211 | 'rpki_status', 212 | 'irr_status', 213 | 'delegated_prefix_status', 214 | 'delegated_asn_status', 215 | 'descr', 216 | 'moas', 217 | 'originasn_name', 218 | 'asn_name') 219 | 220 | 221 | 222 | class NetworkDelaySerializer(serializers.ModelSerializer): 223 | startpoint_type = serializers.CharField(source='startpoint.type') 224 | startpoint_name = serializers.CharField(source='startpoint.name') 225 | startpoint_af = serializers.IntegerField(source='startpoint.af') 226 | endpoint_type = serializers.CharField(source='endpoint.type') 227 | endpoint_name = serializers.CharField(source='endpoint.name') 228 | endpoint_af = serializers.IntegerField(source='endpoint.af') 229 | 230 | class Meta: 231 | model = Atlas_delay 232 | fields = ('timebin', 233 | 'startpoint_type', 234 | 'startpoint_name', 235 | 'startpoint_af', 236 | 'endpoint_type', 237 | 'endpoint_name', 238 | 'endpoint_af', 239 | 'median', 240 | 'nbtracks', 241 | 'nbprobes', 242 | 'entropy', 243 | 'hop', 244 | 'nbrealrtts') 245 | 246 | class NetworkDelayLocationsSerializer(serializers.ModelSerializer): 247 | class Meta: 248 | model = Atlas_location 249 | fields = ('type', 'name', 'af') 250 | 251 | class ASNSerializer(serializers.ModelSerializer): 252 | hegemony = serializers.BooleanField(source='ashash', 253 | help_text='True if participate in AS dependency analysis.') 254 | delay_forwarding = serializers.BooleanField(source='tartiflette', 255 | help_text='True if participate in link delay and forwarding anomaly analysis.') 256 | 257 | class Meta: 258 | model = ASN 259 | fields = ('number', 260 | 'name', 261 | 'hegemony', 262 | 'delay_forwarding', 263 | 'disco') 264 | 265 | class CountrySerializer(serializers.ModelSerializer): 266 | class Meta: 267 | model = Country 268 | fields = ('code', 'name') 269 | 270 | class NetworkDelayAlarmsSerializer(serializers.ModelSerializer): 271 | startpoint_type = serializers.CharField(source='startpoint.type') 272 | startpoint_name = serializers.CharField(source='startpoint.name') 273 | startpoint_af = serializers.IntegerField(source='startpoint.af') 274 | endpoint_type = serializers.CharField(source='endpoint.type') 275 | endpoint_name = serializers.CharField(source='endpoint.name') 276 | endpoint_af = serializers.IntegerField(source='endpoint.af') 277 | 278 | class Meta: 279 | model = Atlas_delay_alarms 280 | fields = ('timebin', 281 | 'startpoint_type', 282 | 'startpoint_name', 283 | 'startpoint_af', 284 | 'endpoint_type', 285 | 'endpoint_name', 286 | 'endpoint_af', 287 | 'deviation') 288 | 289 | class MetisAtlasSelectionSerializer(serializers.ModelSerializer): 290 | asn_name = serializers.CharField(source='asn.name', 291 | help_text="Autonomous System name.") 292 | 293 | class Meta: 294 | model = Metis_atlas_selection 295 | fields = ('timebin', 296 | 'metric', 297 | 'rank', 298 | 'asn', 299 | 'af', 300 | 'asn_name') 301 | 302 | class MetisAtlasDeploymentSerializer(serializers.ModelSerializer): 303 | asn_name = serializers.CharField(source='asn.name', 304 | help_text="Autonomous System name.") 305 | 306 | class Meta: 307 | model = Metis_atlas_deployment 308 | fields = ('timebin', 309 | 'metric', 310 | 'rank', 311 | 'asn', 312 | 'af', 313 | 'nbsamples', 314 | 'asn_name') 315 | 316 | class TRHegemonySerializer(serializers.ModelSerializer): 317 | origin_type = serializers.CharField(source='origin.type') 318 | origin_name = serializers.CharField(source='origin.name') 319 | origin_af = serializers.IntegerField(source='origin.af') 320 | dependency_type = serializers.CharField(source='dependency.type') 321 | dependency_name = serializers.CharField(source='dependency.name') 322 | dependency_af = serializers.IntegerField(source='dependency.af') 323 | 324 | class Meta: 325 | model = TR_hegemony 326 | fields = ('timebin', 327 | 'origin_type', 328 | 'origin_name', 329 | 'origin_af', 330 | 'dependency_type', 331 | 'dependency_name', 332 | 'dependency_af', 333 | 'hege', 334 | 'af', 335 | 'nbsamples') 336 | -------------------------------------------------------------------------------- /static/ihr/js/vendor/modernizr-2.8.3-respond-1.4.2.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.8.3 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b)&&c(b).matches||!1;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f #mq-test-1 { width: 42px; }',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){v(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))},g=function(a){return a.replace(c.regex.minmaxwh,"").match(c.regex.other)};if(c.ajax=f,c.queue=d,c.unsupportedmq=g,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,comments:/\/\*[^*]*\*+([^/][^*]*\*+)*\//gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,maxw:/\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,minmaxwh:/\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi,other:/\([^\)]*\)/g},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var h,i,j,k=a.document,l=k.documentElement,m=[],n=[],o=[],p={},q=30,r=k.getElementsByTagName("head")[0]||l,s=k.getElementsByTagName("base")[0],t=r.getElementsByTagName("link"),u=function(){var a,b=k.createElement("div"),c=k.body,d=l.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=k.createElement("body"),c.style.background="none"),l.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&l.insertBefore(c,l.firstChild),a=b.offsetWidth,f?l.removeChild(c):c.removeChild(b),l.style.fontSize=d,e&&(c.style.fontSize=e),a=j=parseFloat(a)},v=function(b){var c="clientWidth",d=l[c],e="CSS1Compat"===k.compatMode&&d||k.body[c]||d,f={},g=t[t.length-1],p=(new Date).getTime();if(b&&h&&q>p-h)return a.clearTimeout(i),i=a.setTimeout(v,q),void 0;h=p;for(var s in m)if(m.hasOwnProperty(s)){var w=m[s],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?j||u():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?j||u():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(n[w.rules]))}for(var C in o)o.hasOwnProperty(C)&&o[C]&&o[C].parentNode===r&&r.removeChild(o[C]);o.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=k.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,r.insertBefore(E,g.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(k.createTextNode(F)),o.push(E)}},w=function(a,b,d){var e=a.replace(c.regex.comments,"").replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var h=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},i=!f&&d;b.length&&(b+="/"),i&&(f=1);for(var j=0;f>j;j++){var k,l,o,p;i?(k=d,n.push(h(a))):(k=e[j].match(c.regex.findStyles)&&RegExp.$1,n.push(RegExp.$2&&h(RegExp.$2))),o=k.split(","),p=o.length;for(var q=0;p>q;q++)l=o[q],g(l)||m.push({media:l.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:n.length-1,hasquery:l.indexOf("(")>-1,minw:l.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:l.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}v()},x=function(){if(d.length){var b=d.shift();f(b.href,function(c){w(c,b.href,b.media),p[b.href]=!0,a.setTimeout(function(){x()},0)})}},y=function(){for(var b=0;bli>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /static/ihr/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.postgres.fields import JSONField 3 | from model_utils import Choices 4 | from django.contrib.auth.models import PermissionsMixin 5 | from django.contrib.auth.models import Group, Permission 6 | from django.contrib.auth.base_user import AbstractBaseUser 7 | from django.contrib.auth.models import BaseUserManager 8 | 9 | from caching.base import CachingManager, CachingMixin 10 | 11 | class ASN(CachingMixin, models.Model): 12 | number = models.BigIntegerField(primary_key=True, help_text='Autonomous System Number (ASN) or IXP ID. Note that IXP ID are negative to avoid colision.') 13 | name = models.CharField(max_length=255, help_text='Name registered for the network.') 14 | tartiflette = models.BooleanField(default=False, help_text='True if participate in link delay and forwarding anomaly analysis.') 15 | disco = models.BooleanField(default=False, help_text='True if participate in network disconnection analysis.') 16 | ashash = models.BooleanField(default=False, help_text='True if participate in AS dependency analysis.') 17 | 18 | objects = CachingManager() 19 | 20 | class Meta: 21 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 22 | 23 | def __str__(self): 24 | return "ASN%s %s" % (self.number, self.name) 25 | 26 | class Country(CachingMixin, models.Model): 27 | code = models.CharField(max_length=4, primary_key=True) 28 | name = models.CharField(max_length=255) 29 | tartiflette = models.BooleanField(default=False) 30 | disco = models.BooleanField(default=False) 31 | 32 | objects = CachingManager() 33 | 34 | class Meta: 35 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 36 | 37 | def __str__(self): 38 | return "%s (%s)" % (self.name, self.code) 39 | 40 | 41 | # Tartiflette 42 | class Delay(CachingMixin, models.Model): 43 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value.") 44 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, help_text="ASN or IXP ID of the monitored network (see number in /network/).") 45 | magnitude = models.FloatField(default=0.0, help_text="Cumulated link delay deviation. Values close to zero represent usual delays for the network, whereas higher values stand for significant links congestion in the monitored network. ") 46 | 47 | objects = CachingManager() 48 | 49 | class Meta: 50 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 51 | 52 | def __str__(self): 53 | return "%s AS%s" % (self.timebin, self.asn.number) 54 | 55 | 56 | class Delay_alarms(CachingMixin, models.Model): 57 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, db_index=True, help_text="ASN or IXPID of the reported network.") 58 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported alarm.") 59 | ip = models.CharField(max_length=64, db_index=True) 60 | link = models.CharField(max_length=128, db_index=True, help_text="Pair of IP addresses corresponding to the reported link.") 61 | medianrtt = models.FloatField(default=0.0, help_text="Median differential RTT observed during the alarm.") 62 | diffmedian = models.FloatField(default=0.0, help_text="Difference between the link usual median RTT and the median RTT observed during the alarm.") 63 | deviation = models.FloatField(default=0.0, help_text="Distance between observed delays and the past usual values normalized by median absolute deviation.") 64 | nbprobes = models.IntegerField(default=0, help_text="Number of Atlas probes monitoring this link at the reported time window.") 65 | msm_prb_ids = JSONField(default=None, null=True, help_text="List of Atlas measurement IDs and probe IDs used to compute this alarm.") 66 | 67 | objects = CachingManager() 68 | 69 | class Meta: 70 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 71 | 72 | def __str__(self): 73 | return "%s AS%s" % (self.timebin, self.asn.number) 74 | 75 | 76 | 77 | class Forwarding_alarms(CachingMixin, models.Model): 78 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, db_index=True, help_text="ASN or IXPID of the reported network.") 79 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported alarm.") 80 | ip = models.CharField(max_length=64, db_index=True, help_text="Reported IP address, this IP address is seen an unusually high or low number of times in Atlas traceroutes.") 81 | correlation = models.FloatField(default=0.0, help_text="Correlation coefficient between the usual forwarding pattern and the forwarding pattern observed during the alarm. Values range between 0 and -1. Lowest values represent the most anomalous patterns.") 82 | responsibility = models.FloatField(default=0.0, help_text="Responsability score of the reported IP in the forwarding pattern change.") 83 | pktdiff = models.FloatField(default=0.0, help_text="The difference between the number of times the reported IP is seen in traceroutes compare to its usual appearance.") 84 | previoushop = models.CharField(max_length=64, help_text="Last observed IP hop on the usual path.") 85 | msm_prb_ids = JSONField(default=None, null=True, help_text="List of Atlas measurement IDs and probe IDs used to compute this alarm.") 86 | 87 | objects = CachingManager() 88 | 89 | class Meta: 90 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 91 | 92 | def __str__(self): 93 | return "%s AS%s %s" % (self.timebin, self.asn.number, self.ip) 94 | 95 | 96 | class Forwarding(CachingMixin, models.Model): 97 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value.") 98 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, help_text="ASN or IXP ID of the monitored network (see number in /network/).") 99 | magnitude = models.FloatField(default=0.0, help_text="Cumulated link delay deviation. Values close to zero represent usual delays for the network, whereas higher values stand for significant links congestion in the monitored network. ") 100 | 101 | objects = CachingManager() 102 | 103 | class Meta: 104 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 105 | 106 | def __str__(self): 107 | return "%s AS%s" % (self.timebin, self.asn.number) 108 | 109 | 110 | 111 | # Disco 112 | class Disco_events(CachingMixin, models.Model): 113 | mongoid = models.CharField(max_length=24, default="000000000000000000000000", db_index=True) 114 | streamtype = models.CharField(max_length=10, help_text="Granularity of the detected event. The possible values are asn, country, admin1, and admin2. Admin1 represents a wider area than admin2, the exact definition might change from one country to another. For example 'California, US' is an admin1 stream and 'San Francisco County, California, US' is an admin2 stream.") 115 | streamname = models.CharField(max_length=128, help_text="Name of the topological (ASN) or geographical area where the network disconnection happened.") 116 | starttime = models.DateTimeField(help_text="Estimated start time of the network disconnection.") 117 | endtime = models.DateTimeField(help_text="Estimated end time of the network disconnection. Equal to starttime if the end of the event is unknown.") 118 | avglevel = models.FloatField(default=0.0, help_text="Score representing the coordination of disconnected probes. Higher values stand for a large number of Atlas probes that disconnected in a very short time frame. Events with an avglevel lower than 10 are likely to be false positives detection.") 119 | nbdiscoprobes = models.IntegerField(default=0, help_text="Number of Atlas probes that disconnected around the reported start time.") 120 | totalprobes = models.IntegerField(default=0, help_text="Total number of Atlas probes active in the reported stream (ASN, Country, or geographical area).") 121 | ongoing = models.BooleanField(default=False, help_text="Deprecated, this value is unused") 122 | 123 | objects = CachingManager() 124 | 125 | class Meta: 126 | index_together = ("streamtype", "streamname", "starttime", "endtime") 127 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 128 | 129 | class Disco_probes(CachingMixin, models.Model): 130 | probe_id = models.IntegerField(help_text="Atlas probe ID of disconnected probe." ) 131 | event = models.ForeignKey(Disco_events, on_delete=models.CASCADE, db_index=True, related_name="discoprobes", help_text="ID of the network disconnection event where this probe is reported.") 132 | starttime = models.DateTimeField(help_text="Probe disconnection time.") 133 | endtime = models.DateTimeField(help_text="Reconnection time of the probe, this may not be reported if other probes have reconnected earlier.") 134 | level = models.FloatField(default=0.0, help_text="Disconnection level when the probe disconnected.") 135 | ipv4 = models.CharField(max_length=64, default="None", help_text="Public IP address of the Atlas probe.") 136 | prefixv4 = models.CharField(max_length=70, default="None", help_text="IP prefix corresponding the probe.") 137 | lat = models.FloatField(default=0.0, help_text="Latitude of the probe during the network detection as reported by RIPE Altas.") 138 | lon = models.FloatField(default=0.0, help_text="Longitude of the probe during the network detection as reported by RIPE Altas.") 139 | 140 | objects = CachingManager() 141 | 142 | class Meta: 143 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 144 | 145 | 146 | class Hegemony(CachingMixin, models.Model): 147 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value.") 148 | originasn = models.ForeignKey(ASN, on_delete=models.CASCADE, related_name="local_graph", db_index=True, help_text="Dependent network, it can be any public ASN. Retrieve all dependencies of a network by setting only this parameter and a timebin.") 149 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, db_index=True, help_text="Dependency. Transit network commonly seen in BGP paths towards originasn.") 150 | hege = models.FloatField(default=0.0, help_text="AS Hegemony is the estimated fraction of paths towards the originasn. The values range between 0 and 1, low values represent a small number of path (low dependency) and values close to 1 represent strong dependencies.") 151 | af = models.IntegerField(default=0, help_text="Address Family (IP version), values are either 4 or 6.") 152 | 153 | objects = CachingManager() 154 | 155 | class Meta: 156 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 157 | 158 | def __str__(self): 159 | return "%s originAS%s AS%s %s" % (self.timebin, self.originasn.number, self.asn.number, self.hege) 160 | 161 | class HegemonyCone(CachingMixin, models.Model): 162 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value.") 163 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, db_index=True, help_text="Autonomous System Number (ASN).") 164 | conesize = models.IntegerField(default=0, help_text="Number of dependent networks, namely, networks that are reached through the asn, this is similar to CAIDA's customer cone size. The detailed list of all dependent networks is obtained by querying /hegemony/ with parameter asn (e.g /hegemony/?asn=2497&timebin=2020-03-01 gives IIJ's customer networks).") 165 | af = models.IntegerField(default=0, help_text="Address Family (IP version), values are either 4 or 6.") 166 | 167 | objects = CachingManager() 168 | 169 | class Meta: 170 | index_together = ("timebin", "asn", "af") 171 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 172 | 173 | class Hegemony_country(CachingMixin, models.Model): 174 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value.") 175 | country = models.ForeignKey(Country, on_delete=models.CASCADE, db_index=True, help_text="Monitored country. Retrieve all dependencies of a country by setting only this parameter and a timebin.") 176 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, db_index=True, help_text="Dependency. Network commonly seen in BGP paths towards monitored country.") 177 | hege = models.FloatField(default=0.0, help_text="AS Hegemony is the estimated fraction of paths towards the monitored country. The values range between 0 and 1, low values represent a small number of path (low dependency) and values close to 1 represent strong dependencies.") 178 | af = models.IntegerField(default=0, help_text="Address Family (IP version), values are either 4 or 6.") 179 | weight = models.FloatField(default=0.0, help_text="Absolute weight given to the ASN for the AS Hegemony calculation.") 180 | weightscheme = models.CharField(max_length=16, default="None", help_text="Weighting scheme used for the AS Hegemony calculation.") 181 | transitonly = models.BooleanField(default=False, help_text="If True, then origin ASNs of BGP path are ignored (focus only on transit networks).") 182 | 183 | objects = CachingManager() 184 | 185 | class Meta: 186 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 187 | 188 | def __str__(self): 189 | return "%s %s AS%s %s" % (self.timebin, self.country.name, self.asn.number, self.hege) 190 | 191 | 192 | class Hegemony_prefix(CachingMixin, models.Model): 193 | id = models.BigIntegerField(unique=True, primary_key=True) 194 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value.") 195 | prefix = models.CharField(max_length=64, db_index=True, help_text="Monitored prefix (IPv4 or IPv6).") 196 | originasn = models.ForeignKey(ASN, on_delete=models.CASCADE, related_name="prefix_originasn", db_index=True, help_text="Network seen as originating the monitored prefix.") 197 | country = models.ForeignKey(Country, on_delete=models.CASCADE, db_index=True, help_text="Country for the monitored prefix identified by Maxmind's Geolite2 geolocation database.") 198 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, related_name="prefix_asn", help_text="Dependency. Network commonly seen in BGP paths towards monitored prefix.") 199 | hege = models.FloatField(default=0.0, help_text="AS Hegemony is the estimated fraction of paths towards the monitored prefix. The values range between 0 and 1, low values represent a small number of path (low dependency) and values close to 1 represent strong dependencies.") 200 | af = models.IntegerField(default=0, help_text="Address Family (IP version), values are either 4 or 6.") 201 | visibility = models.FloatField(default=0.0, help_text="Percentage of BGP peers that see this prefix.") 202 | rpki_status = models.CharField(max_length=32, help_text="Route origin validation state for the monitored prefix and origin AS using RPKI.") 203 | irr_status = models.CharField(max_length=32, help_text="Route origin validation state for the monitored prefix and origin AS using IRR.") 204 | delegated_prefix_status = models.CharField(max_length=32, help_text="Status of the monitored prefix in the RIR's delegated stats. Status other than 'assigned' are usually considered as bogons.") 205 | delegated_asn_status = models.CharField(max_length=32, help_text="Status of the origin ASN in the RIR's delegated stats. Status other than 'assigned' are usually considered as bogons.") 206 | descr = models.CharField(max_length=64, help_text="Prefix description from IRR (maximum 64 characters).") 207 | moas = models.BooleanField(default=False, help_text="True if the prefix is originated by multiple ASNs.") 208 | 209 | objects = CachingManager() 210 | 211 | class Meta: 212 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 213 | 214 | def __str__(self): 215 | return "%s %s AS%s %s" % (self.timebin, self.prefix, self.originasn.number, self.hege) 216 | 217 | 218 | class Atlas_location(CachingMixin, models.Model): 219 | name = models.CharField(max_length=255, help_text="Location identifier. The meaning of values dependend on the location type:
  • type=AS: ASN
  • type=CT: city name, region name, country code
  • type=PB: Atlas Probe ID
  • type=IP: IP version (4 or 6)
") 220 | type = models.CharField(max_length=4, help_text="Type of location. Possible values are:
  • AS: Autonomous System
  • CT: City
  • PB: Atlas Probe
  • IP: Whole IP space
") 221 | af = models.IntegerField(help_text="Address Family (IP version), values are either 4 or 6.") 222 | 223 | objects = CachingManager() 224 | 225 | class Meta: 226 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 227 | 228 | def __str__(self): 229 | return "(%s) %s %s" % (self.type, self.name, self.af) 230 | 231 | 232 | class Atlas_delay(CachingMixin, models.Model): 233 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value.") 234 | startpoint = models.ForeignKey(Atlas_location, on_delete=models.CASCADE, 235 | db_index=True, related_name='location_startpoint', help_text="Starting location for the delay estimation.") 236 | endpoint = models.ForeignKey(Atlas_location, on_delete=models.CASCADE, 237 | db_index=True, related_name='location_endpoint', help_text="Ending location for the delay estimation.") 238 | median = models.FloatField(default=0.0, help_text="Estimated median RTT. RTT values are directly extracted from traceroute (a.k.a. realrtts) and estimated via differential RTTs.") 239 | nbtracks = models.IntegerField(default=0, help_text="Number of RTT samples used to compute median RTT (either real or differential RTT).") 240 | nbprobes = models.IntegerField(default=0, help_text="Number of Atlas probes used to compute median RTT.") 241 | entropy = models.FloatField(default=0.0, help_text="Entropy of RTT samples with regards to probes' ASN. Values close to zero mean that Atlas probes used for these measures are located in the same AS, values close to one means that preobes are equally spread out accross multiple ASes.") 242 | hop = models.IntegerField(default=0, help_text="Median number of AS hops between the start and end locations.") 243 | nbrealrtts = models.IntegerField(default=0, help_text="Number of RTT samples directly obtained from traceroutes (as opposed to differential RTTs).") 244 | 245 | objects = CachingManager() 246 | 247 | class Meta: 248 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 249 | 250 | def __str__(self): 251 | return "%s -> %s: %s" % ( 252 | self.startpoint.name, self.endpoint.name, self.median) 253 | 254 | class Hegemony_alarms(CachingMixin, models.Model): 255 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported alarm.") 256 | originasn = models.ForeignKey(ASN, on_delete=models.CASCADE, related_name="anomalous_originasn", db_index=True, help_text="ASN of the reported dependent network.") 257 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, related_name="anomalous_asn", db_index=True, help_text="ASN of the anomalous dependency (transit network).") 258 | deviation = models.FloatField(default=0.0, help_text="Significance of the AS Hegemony change.") 259 | af = models.IntegerField(help_text="Address Family (IP version), values are either 4 or 6.") 260 | 261 | objects = CachingManager() 262 | 263 | class Meta: 264 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 265 | 266 | def __str__(self): 267 | return "(%s, %s, v%s) %s" % (self.originasn, self.asn, self.af, self.deviation) 268 | 269 | class Atlas_delay_alarms(CachingMixin, models.Model): 270 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported alarm.") 271 | startpoint = models.ForeignKey(Atlas_location, on_delete=models.CASCADE, 272 | db_index=True, related_name='anomalous_startpoint', help_text="Starting location reported as anomalous.") 273 | endpoint = models.ForeignKey(Atlas_location, on_delete=models.CASCADE, 274 | db_index=True, related_name='anomalous_endpoint', help_text="Ending location reported as anomalous.") 275 | deviation = models.FloatField(default=0.0, help_text="Significance of the AS Hegemony change.") 276 | 277 | objects = CachingManager() 278 | 279 | class Meta: 280 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 281 | 282 | def __str__(self): 283 | return "(%s, %s) %s" % (self.startpoint, self.endpoint, self.deviation) 284 | 285 | #user 286 | class UserManager(BaseUserManager): 287 | def _create_user(self, email, password): 288 | if not email or not password: 289 | raise ValueError('The email and password must be set') 290 | email = self.normalize_email(email) 291 | user = self.model(email=email) 292 | user.set_password(password) 293 | user.is_active = False 294 | user.save() 295 | return user 296 | 297 | def create_user(self, email, password, **extra_fields): 298 | """ 299 | Create a User. 300 | """ 301 | if not email: 302 | raise ValueError('The Email must be set') 303 | 304 | extra_fields.setdefault('is_staff', False) 305 | extra_fields.setdefault('is_superuser', False) 306 | return self._create_user(email, password) 307 | 308 | def create_superuser(self, email, password, **extra_fields): 309 | """ 310 | Create a super User. 311 | """ 312 | extra_fields.setdefault('is_staff', True) 313 | extra_fields.setdefault('is_superuser', True) 314 | 315 | if extra_fields.get('is_staff') is not True: 316 | raise ValueError('Superuser must have is_staff=True.') 317 | if extra_fields.get('is_superuser') is not True: 318 | raise ValueError('Superuser must have is_superuser=True.') 319 | 320 | return self._create_user(email, password) 321 | 322 | 323 | class IHRUser(AbstractBaseUser, PermissionsMixin): 324 | email = models.EmailField(unique=True, help_text='Email of the user, also used a the login ID.') 325 | groups = models.ManyToManyField( 326 | Group, 327 | blank=True, 328 | related_name="ihruser_set", 329 | related_query_name="ihruser", 330 | ) 331 | user_permissions = models.ManyToManyField( 332 | Permission, 333 | blank=True, 334 | related_name="ihruser_set", 335 | related_query_name="ihruser", 336 | ) 337 | is_active = models.BooleanField(default=False) 338 | # last time requested for password 339 | objects = UserManager() 340 | monitoringasn = models.ManyToManyField( 341 | 'ASN', through='MonitoredASN') 342 | 343 | USERNAME_FIELD = 'email' 344 | REQUIRED_FIELDS = [] 345 | 346 | class IHRUser_Channel(CachingMixin,models.Model): 347 | name = models.CharField(max_length=255) 348 | channel = models.CharField(max_length=255) 349 | frequency = models.CharField(max_length=255,default="normal") 350 | 351 | objects = CachingManager() 352 | 353 | class Meta: 354 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 355 | def __str__(self): 356 | return "%s (%s)" % (self.name,self.channel) 357 | 358 | 359 | class EmailChangeRequest(models.Model): 360 | """ 361 | This model permit to change the email leaving the IHRUser model DRY. 362 | store change requests so can be verified by the change entrypoint 363 | """ 364 | user = models.ForeignKey(IHRUser, on_delete=models.CASCADE) 365 | new_email = models.EmailField(unique=True) 366 | request_time = models.DateTimeField(auto_now_add=True) 367 | 368 | VALIDITY = 60 * 24 #expressed in minutes 369 | 370 | 371 | class MonitoredASN(models.Model): 372 | NOTIFY_LEVEL = Choices((0, 'LOW', 'low'), (5, 'MODERATE', 'moderate'), (10, 'HIGH', 'high')) 373 | user = models.ForeignKey(IHRUser, on_delete=models.CASCADE) 374 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE) 375 | 376 | notifylevel = models.SmallIntegerField( 377 | choices=NOTIFY_LEVEL, 378 | default=NOTIFY_LEVEL.HIGH 379 | ) 380 | 381 | class Metis_atlas_selection(CachingMixin, models.Model): 382 | """ 383 | Metis helps to select a set of diverse Atlas probes in terms of different 384 | topological metrics (e.g. AS path, RTT). 385 | """ 386 | timebin = models.DateTimeField(db_index=True, help_text="Time when the ranking is computed. The ranking uses four weeks of data, hence 2022-03-28T00:00 means the ranking using data from 2022-02-28T00:00 to 2022-03-28T00:00.") 387 | metric = models.CharField(max_length=16, help_text="Distance metric used to compute diversity, possible values are: 'as_path_length', 'ip_hops', 'rtt'") 388 | rank = models.IntegerField(help_text="Selecting all ASes with rank less than equal to 10 (i.e. rank__lte=10), gives the 10 most diverse ASes in terms of the selected metric.") 389 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, db_index=True, help_text="Atlas probes' Autonomous System Number.") 390 | af = models.IntegerField(help_text="Address Family (IP version), values are either 4 or 6.") 391 | mean = models.FloatField(default=0.0, help_text="The mean distance value (e.g., AS-path length) we get when using all ASes up to this rank. This decreases with increasing rank, since lower ranks represent closer ASes.") 392 | 393 | objects = CachingManager() 394 | 395 | class Meta: 396 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 397 | 398 | class Metis_atlas_deployment(CachingMixin, models.Model): 399 | """ 400 | Metis identifies ASes that are far from Atlas probes. Deploying Atlas probes 401 | in these ASes would be beneficial for Atlas coverage. 402 | """ 403 | timebin = models.DateTimeField(db_index=True, help_text="Time when the ranking is computed. The ranking uses 24 weeks of data, hence 2022-05-23T00:00 means the ranking using data from 2021-12-06T00:00 to 2022-05-23T00:00.") 404 | metric = models.CharField(max_length=16, help_text="Distance metric used to compute diversity, possible values are: 'as_path_length', 'ip_hops', 'rtt'") 405 | rank = models.IntegerField(help_text="Selecting all ASes with rank less than equal to 10 (i.e. rank__lte=10), gives the 10 most diverse ASes in terms of the selected metric.") 406 | asn = models.ForeignKey(ASN, on_delete=models.CASCADE, db_index=True, help_text="Atlas probes' Autonomous System Number.") 407 | af = models.IntegerField(help_text="Address Family (IP version), values are either 4 or 6.") 408 | mean = models.FloatField(default=0.0, help_text="The mean distance value (e.g., AS-path length) we get when using all ASes up to this rank. This decreases with increasing rank, since lower ranks represent closer ASes.") 409 | nbsamples = models.IntegerField(default=0, help_text="The number of probe ASes for which we have traceroutes to this AS in the time interval. We currently only include candidates that were reached by at least 50% of probe ASes, hence these values are always large.") 410 | 411 | objects = CachingManager() 412 | 413 | class Meta: 414 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 415 | 416 | # Traceroute dependencies 417 | class TR_hegemony_identifier(CachingMixin, models.Model): 418 | name = models.CharField(max_length=255, help_text="Value of the identifier. The meaning depends on the identifier type:
  • type=AS: ASN
  • type=IX: PeeringDB IX ID
  • type=MB: IXP member (format: ix_id;asn)
  • type=IP: Interface IP of an IXP member
") 419 | type = models.CharField(max_length=4, help_text="Type of the identifier. Possible values are:
  • AS: Autonomous System
  • IX: IXP
  • MB: IXP member
  • IP: IXP member IP
") 420 | af = models.IntegerField(help_text="Address family (IP version), values are either 4 or 6.") 421 | 422 | objects = CachingManager() 423 | 424 | class Meta: 425 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 426 | 427 | def __str__(self): 428 | return "(%s) %s %s" % (self.type, self.name, self.af) 429 | 430 | 431 | class TR_hegemony(CachingMixin, models.Model): 432 | timebin = models.DateTimeField(db_index=True, help_text="Timestamp of reported value. The computation uses four weeks of data, hence 2022-03-28T00:00 means the values are based on data from 2022-02-28T00:00 to 2022-03-28T00:00.") 433 | origin = models.ForeignKey(TR_hegemony_identifier, on_delete=models.CASCADE, db_index=True, related_name='local_graph', 434 | help_text="Dependent network, it can be any public ASN. Retrieve all dependencies of a network by setting only this parameter and a timebin.") 435 | dependency = models.ForeignKey(TR_hegemony_identifier, on_delete=models.CASCADE, db_index=True, 436 | help_text="Dependency. Transit network or IXP commonly seen in traceroutes towards the origin.") 437 | hege = models.FloatField(default=0.0, help_text="AS Hegemony is the estimated fraction of paths towards the origin. The values range between 0 and 1, low values represent a small number of path (low dependency) and values close to 1 represent strong dependencies.") 438 | af = models.IntegerField(default=0, help_text="Address family (IP version), values are either 4 or 6.") 439 | nbsamples = models.IntegerField(default=0, help_text="The number of probe ASes for which we have traceroutes to the origin in the time interval. We only include AS Hegemony values that are based on traceroutes from at least ten probe ASes.") 440 | 441 | objects = CachingManager() 442 | 443 | class Meta: 444 | base_manager_name = 'objects' # Attribute name of CachingManager(), above 445 | 446 | def __str__(self): 447 | return f'{self.timebin} origin {self.origin.type} {self.origin.name} dep {self.dependency.type} {self.dependency.name} hege {self.hege} nbsamples {self.nbsamples}' 448 | 449 | 450 | # TODO Remove this? 451 | 452 | class Delay_alarms_msms(models.Model): 453 | alarm = models.ForeignKey(Delay_alarms, related_name="msmid", 454 | on_delete=models.CASCADE) 455 | msmid = models.BigIntegerField(default=0) 456 | probeid = models.IntegerField(default=0) 457 | 458 | def __str__(self): 459 | return "%s %s" % (self.msmid, self.probeid) 460 | 461 | 462 | class Forwarding_alarms_msms(models.Model): 463 | alarm = models.ForeignKey(Forwarding_alarms, related_name="msmid", 464 | on_delete=models.CASCADE) 465 | msmid = models.BigIntegerField(default=0) 466 | probeid = models.IntegerField(default=0) 467 | 468 | def __str__(self): 469 | return "%s %s" % (self.msmid, self.probeid) 470 | 471 | --------------------------------------------------------------------------------