├── app ├── __init__.py ├── marks │ ├── __init__.py │ ├── config.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── marksapp │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_misc.py │ │ └── test_pagination.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0007_auto_20170409_1724.py │ │ ├── 0013_bookmark_last_bumped.py │ │ ├── 0009_bookmark_description.py │ │ ├── 0012_auto_20181010_1747.py │ │ ├── 0006_auto_20170409_1713.py │ │ ├── 0010_auto_20180127_0908.py │ │ ├── 0003_auto_20170409_1629.py │ │ ├── 0008_auto_20170414_1238.py │ │ ├── 0004_auto_20170409_1630.py │ │ ├── 0005_auto_20170409_1652.py │ │ ├── 0002_bookmark_user.py │ │ ├── 0011_profile.py │ │ └── 0001_initial.py │ ├── templatetags │ │ ├── __init__.py │ │ └── filters.py │ ├── apps.py │ ├── static │ │ └── marksapp │ │ │ ├── favicon │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-150x150.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-256x256.png │ │ │ ├── browserconfig.xml │ │ │ ├── site.webmanifest │ │ │ └── safari-pinned-tab.svg │ │ │ ├── style.css │ │ │ └── js │ │ │ ├── app.js │ │ │ └── jquery.min.js │ ├── admin.py │ ├── templates │ │ ├── edit_mark_form.html │ │ ├── comma_tags.html │ │ ├── registration │ │ │ ├── logout.html │ │ │ ├── registration_form.html │ │ │ └── login.html │ │ ├── add.html │ │ ├── import.html │ │ ├── mark_permalink.html │ │ ├── profile.html │ │ ├── mark.html │ │ ├── index.html │ │ ├── guide.html │ │ ├── marks.html │ │ └── base.html │ ├── misc.py │ ├── backends.py │ ├── models.py │ ├── management │ │ └── commands │ │ │ ├── read_file.py │ │ │ └── setupfromfile.py │ ├── netscape.py │ ├── urls.py │ ├── forms.py │ └── views.py ├── Dockerfile ├── wsgi.py ├── reqs.txt ├── docker-compose.yml ├── manage.py └── etc │ └── changelog.markdown ├── webextension ├── icons │ ├── icon-24.png │ ├── icon-32.png │ ├── icon-48.png │ └── icon-96.png ├── popup │ ├── actions.html │ ├── actions.css │ └── actions.js ├── content-script.js ├── manifest.json └── background-script.js ├── LICENSE.md ├── README.md └── .gitignore /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/marks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/marksapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/marksapp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/marksapp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/marksapp/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/marks/config.py: -------------------------------------------------------------------------------- 1 | DB_NAME = "marks" 2 | DB_USER = "postgres" 3 | DB_PW = "postgres" 4 | DB_PORT = 5432 5 | -------------------------------------------------------------------------------- /webextension/icons/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/webextension/icons/icon-24.png -------------------------------------------------------------------------------- /webextension/icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/webextension/icons/icon-32.png -------------------------------------------------------------------------------- /webextension/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/webextension/icons/icon-48.png -------------------------------------------------------------------------------- /webextension/icons/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/webextension/icons/icon-96.png -------------------------------------------------------------------------------- /app/marksapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MarksappConfig(AppConfig): 5 | name = "marksapp" 6 | -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/app/marksapp/static/marksapp/favicon/favicon.ico -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/app/marksapp/static/marksapp/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/app/marksapp/static/marksapp/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/app/marksapp/static/marksapp/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/app/marksapp/static/marksapp/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/app/marksapp/static/marksapp/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelipeCortez/bmarks/HEAD/app/marksapp/static/marksapp/favicon/android-chrome-256x256.png -------------------------------------------------------------------------------- /app/marksapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Bookmark, Tag, Profile 3 | 4 | admin.site.register(Bookmark) 5 | admin.site.register(Tag) 6 | admin.site.register(Profile) 7 | -------------------------------------------------------------------------------- /app/marksapp/templates/edit_mark_form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% csrf_token %} 4 | {{ form }} 5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /app/marksapp/templates/comma_tags.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/marksapp/templates/registration/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_info %} 4 | 5 | · 6 | logout 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |

You have been logged out.

12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /webextension/popup/actions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #5baf68 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN mkdir /code 6 | 7 | WORKDIR /code 8 | 9 | ADD reqs.txt /code/ 10 | 11 | RUN apk update && \ 12 | apk add postgresql-libs && \ 13 | apk add --virtual .build-deps gcc musl-dev postgresql-dev && \ 14 | python3 -m pip install -r reqs.txt --no-cache-dir && \ 15 | apk --purge del .build-deps 16 | 17 | ADD . /code/ 18 | -------------------------------------------------------------------------------- /app/marksapp/templates/add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_info %} 4 | 5 | · 6 | add 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 | {% csrf_token %} 15 | {{ form }} 16 | 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /app/marksapp/misc.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # optional dot, word characters, hyphens allowed 4 | tag_regex = "^\.?[-\w]+$" 5 | # same but repeated and joined by + 6 | multitag_regex = "^(\+?\.?[-\w]+)+$" 7 | 8 | 9 | def website_from_url(url: str): 10 | # idea: reverse url, look from last / to start 11 | match = re.match(r"^(?:\w+:\/\/)?(?:www\.)?([\w.]+).*", url) 12 | 13 | if match and match.group(1): 14 | return match.group(1) 15 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0007_auto_20170409_1724.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-09 20:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("marksapp", "0006_auto_20170409_1713")] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether(name="bookmark", unique_together=set([])) 14 | ] 15 | -------------------------------------------------------------------------------- /app/marksapp/templates/import.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_info %} 4 | 5 | · 6 | import 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 | {% csrf_token %} 15 | {{ form }} 16 | 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for marks 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.10/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", "marks.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/marks/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for marks 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.10/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", "marks.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/reqs.txt: -------------------------------------------------------------------------------- 1 | cycler==0.10.0 2 | decorator==4.0.11 3 | Django==2.0.4 4 | django-debug-toolbar==1.9.1 5 | django-registration==2.2 6 | djangorestframework==3.6.2 7 | Markdown==3.0.1 8 | pexpect==4.2.1 9 | pickleshare==0.7.4 10 | prompt-toolkit==1.0.13 11 | psycopg2==2.8.3 12 | ptyprocess==0.5.1 13 | Pygments==2.2.0 14 | pyparsing==2.2.0 15 | python-dateutil==2.6.0 16 | pytz==2017.2 17 | simplegeneric==0.8.1 18 | six==1.10.0 19 | sqlparse==0.2.4 20 | traitlets==4.3.2 21 | wcwidth==0.1.7 22 | wheel==0.29.0 23 | -------------------------------------------------------------------------------- /app/marks/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | from django.shortcuts import get_object_or_404, get_list_or_404 4 | from django.db.models import Count 5 | from django.conf import settings 6 | from marksapp.models import Bookmark, Tag 7 | 8 | urlpatterns = [url(r"^", include("marksapp.urls")), url(r"^admin/", admin.site.urls)] 9 | 10 | if settings.DEBUG: 11 | import debug_toolbar 12 | 13 | urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))] 14 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0013_bookmark_last_bumped.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2019-09-21 13:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('marksapp', '0012_auto_20181010_1747'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='bookmark', 15 | name='last_bumped', 16 | field=models.DateTimeField(blank=True, null=True, verbose_name='last bumped'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0009_bookmark_description.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-12-17 13:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("marksapp", "0008_auto_20170414_1238")] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="bookmark", 15 | name="description", 16 | field=models.TextField(blank=True), 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bmarks", 3 | "short_name": "bmarks", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#5baf68", 17 | "background_color": "#5baf68", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0012_auto_20181010_1747.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-10-10 17:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("marksapp", "0011_profile")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="profile", 13 | name="visibility", 14 | field=models.CharField( 15 | choices=[("PB", "Public"), ("PV", "Private")], 16 | default="PB", 17 | max_length=2, 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0006_auto_20170409_1713.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-09 20:13 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ("marksapp", "0005_auto_20170409_1652"), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterUniqueTogether( 18 | name="bookmark", unique_together=set([("user", "url")]) 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | 3 | services: 4 | db: 5 | image: postgres:10-alpine 6 | volumes: 7 | - postgres_data:/var/lib/postgresql/data/ 8 | environment: 9 | - POSTGRES_USER=postgres 10 | - POSTGRES_PASSWORD=postgres 11 | - POSTGRES_DB=marks 12 | restart: always 13 | web: 14 | build: . 15 | command: python3 manage.py runserver 0.0.0.0:8000 16 | volumes: 17 | - .:/code 18 | ports: 19 | - "8000:8000" 20 | environment: 21 | - DJANGO_DEVELOPMENT 22 | depends_on: 23 | - db 24 | cpus: 0.75 25 | restart: always 26 | 27 | volumes: 28 | postgres_data: 29 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0010_auto_20180127_0908.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2018-01-27 12:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("marksapp", "0009_bookmark_description")] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions(name="tag", options={"ordering": ["name"]}), 14 | migrations.AlterField( 15 | model_name="bookmark", 16 | name="url", 17 | field=models.CharField(blank=True, max_length=512), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /app/marksapp/backends.py: -------------------------------------------------------------------------------- 1 | # thanks https://gist.github.com/jbittel/5181683 2 | 3 | from django.contrib.auth.backends import ModelBackend 4 | from django.contrib.auth.models import User 5 | 6 | 7 | class CaseInsensitiveModelBackend(ModelBackend): 8 | def authenticate(self, username=None, password=None): 9 | try: 10 | user = User.objects.get(username__iexact=username) 11 | if user.check_password(password): 12 | return user 13 | except User.DoesNotExist: 14 | return None 15 | 16 | def get_user(self, user_id): 17 | try: 18 | return User.objects.get(pk=user_id) 19 | except User.DoesNotExist: 20 | return None 21 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0003_auto_20170409_1629.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-09 19:29 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [("marksapp", "0002_bookmark_user")] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="bookmark", 17 | name="user", 18 | field=models.OneToOneField( 19 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 20 | ), 21 | ) 22 | ] 23 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0008_auto_20170414_1238.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-14 15:38 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [("marksapp", "0007_auto_20170409_1724")] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="bookmark", 17 | name="user", 18 | field=models.ForeignKey( 19 | default=1, 20 | on_delete=django.db.models.deletion.CASCADE, 21 | to=settings.AUTH_USER_MODEL, 22 | ), 23 | ) 24 | ] 25 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0004_auto_20170409_1630.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-09 19:30 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [("marksapp", "0003_auto_20170409_1629")] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="bookmark", 17 | name="user", 18 | field=models.OneToOneField( 19 | blank=True, 20 | null=True, 21 | on_delete=django.db.models.deletion.CASCADE, 22 | to=settings.AUTH_USER_MODEL, 23 | ), 24 | ) 25 | ] 26 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0005_auto_20170409_1652.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-09 19:52 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [("marksapp", "0004_auto_20170409_1630")] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="bookmark", 17 | name="user", 18 | field=models.ForeignKey( 19 | blank=True, 20 | null=True, 21 | on_delete=django.db.models.deletion.CASCADE, 22 | to=settings.AUTH_USER_MODEL, 23 | ), 24 | ) 25 | ] 26 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0002_bookmark_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-09 19:16 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ("marksapp", "0001_initial"), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name="bookmark", 20 | name="user", 21 | field=models.OneToOneField( 22 | default=1, 23 | on_delete=django.db.models.deletion.CASCADE, 24 | to=settings.AUTH_USER_MODEL, 25 | ), 26 | ) 27 | ] 28 | -------------------------------------------------------------------------------- /app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marks.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /app/marksapp/templatetags/filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.defaultfilters import stringfilter 3 | from markdown.extensions import Extension 4 | import markdown as mdown 5 | 6 | 7 | class EscapeHtml(Extension): 8 | def extendMarkdown(self, md): 9 | md.preprocessors.deregister("html_block") 10 | md.inlinePatterns.deregister("html") 11 | 12 | 13 | register = template.Library() 14 | 15 | 16 | @register.filter(name="remove_tag") 17 | def remove_tag(value, tag): 18 | tags = value[:] 19 | tags.remove(tag) 20 | return "+".join(tags) 21 | 22 | 23 | @register.filter(name="add_tag") 24 | def add_tag(value, new_tag): 25 | tags = value[:] 26 | tags.append(new_tag) 27 | return "+".join(tags) 28 | 29 | 30 | @register.filter(name="markdown") 31 | @stringfilter 32 | def markdown(value): 33 | return mdown.markdown(value, extensions=[EscapeHtml()]) 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Felipe Cortez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /app/marksapp/templates/mark_permalink.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load filters %} 3 | 4 | {% block page_info %} 5 | 6 | · 7 | {{ mark.user.username }} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bmarks 2 | 3 | A bookmarking tool made with Python 3 + Django 2.0. Running [here](https://bmarks.net/felipecortez). 4 | 5 | ## Features 6 | 7 | - [x] Import bookmarks saved in the Netscape format 8 | - [x] Tag completion in forms 9 | - [x] Private marks with `private` tag 10 | - [x] Private accounts 11 | - [x] Descriptions with Markdown support 12 | - [x] A friendly user guide (not so friendly yet) 13 | - [x] A bookmarklet 14 | - [x] Unlisted marks with dot prefix 15 | - [x] [Browser extension](https://addons.mozilla.org/en-US/firefox/addon/bmarks/?src=userprofile) (Firefox only currently) 16 | - [x] Bulk editing 17 | - [x] Export bookmarks (JSON, CSV) 18 | - [x] [Wayback Machine](https://archive.org/web/) Availability API integration 19 | - [ ] Archive with full-text search 20 | - [ ] Toggle between compact/one-line and spacious/multi-line views 21 | 22 | ## Bookmarklet 23 | 24 | ```javascript:location.href='https://bmarks.net/add/?url='+encodeURIComponent(location.href)+'&name='+encodeURIComponent(document.title)``` 25 | 26 | ## License 27 | 28 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 29 | -------------------------------------------------------------------------------- /webextension/content-script.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (window.hasRun) { 3 | return; 4 | } 5 | 6 | window.hasRun = true; 7 | 8 | browser.runtime.onMessage.addListener(message => { 9 | switch (message.command) { 10 | case 'insertCurrentTab': 11 | insertCurrentTab(message.title, message.url); break; 12 | case 'insertAllTabs': 13 | insertAllTabs(message.description); break; 14 | } 15 | }); 16 | 17 | function insertCurrentTab(title, url) { 18 | let urlField = document.querySelector('#id_url'); 19 | urlField.value = url; 20 | 21 | let titleField = document.querySelector('#id_name'); 22 | titleField.value = title; 23 | titleField.focus(); 24 | } 25 | 26 | function insertAllTabs(description) { 27 | let tagsField = document.querySelector('#id_tags'); 28 | tagsField.value = '.tab-collection'; 29 | 30 | let descriptionField = document.querySelector('#id_description'); 31 | descriptionField.value = description; 32 | descriptionField.style.whiteSpace = 'pre'; 33 | descriptionField.style.height = `${ descriptionField.scrollHeight + 32 }px`; 34 | 35 | let titleField = document.querySelector('#id_name'); 36 | titleField.focus(); 37 | } 38 | })(); 39 | -------------------------------------------------------------------------------- /app/marksapp/templates/registration/registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_info %} 4 | 5 | · 6 | register 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% if not user.is_authenticated %} 12 |
13 |
14 | {% csrf_token %} 15 | {{ form.non_field_errors }} 16 | 17 | {{ form.username }} 18 | 19 | 20 | {{ form.password }} 21 | 22 | 23 | {{ form.email }} 24 | 25 | 26 | {% for radio in form.visibility %} 27 |
28 | {{ radio }} 29 |
30 | {% endfor %} 31 | 32 | 33 | {% else %} 34 |

Huh... You're already authenticated.

35 | {% endif %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/marksapp/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_info %} 4 | 5 | · 6 | login 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 | {% if not user.is_authenticated %} 13 | {% if next %} 14 | {% if user.is_authenticated %} 15 |

Your account doesn't have access to this page. To proceed, 16 | please login with an account that has access.

17 | {% else %} 18 |

Please login to see this page.

19 | {% endif %} 20 | {% endif %} 21 | 22 |
23 |
24 | {% csrf_token %} 25 | {{ form.non_field_errors }} 26 | 27 | {{ form.username }} 28 | 29 | {{ form.password }} 30 | 31 | 32 | 33 |
34 | {% else %} 35 |

Huh... You're already authenticated.

36 | {% endif %} 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /webextension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "bmarks", 4 | "author": "Felipe Cortez", 5 | "version": "1.2", 6 | "description": "Add bookmarks and tab collections to bmarks.net", 7 | "homepage_url": "https://bmarks.net", 8 | 9 | "permissions": [ 10 | "tabs", 11 | "activeTab", 12 | "*://bmarks.net/*" 13 | ], 14 | 15 | "content_scripts": [ 16 | { 17 | "matches": ["*://bmarks.net/*"], 18 | "js": ["/content-script.js"] 19 | } 20 | ], 21 | 22 | "icons": { 23 | "24": "icons/icon-24.png", 24 | "32": "icons/icon-32.png", 25 | "48": "icons/icon-48.png", 26 | "96": "icons/icon-96.png" 27 | }, 28 | 29 | "background": { 30 | "scripts": ["background-script.js"] 31 | }, 32 | 33 | "browser_action": { 34 | "default_icon": "icons/icon-24.png", 35 | "default_title": "bmarks", 36 | "default_popup": "popup/actions.html" 37 | }, 38 | 39 | "commands": { 40 | "bookmark-current": { 41 | "suggested_key": { 42 | "default": "Alt+Shift+T" 43 | }, 44 | "description": "This tab" 45 | }, 46 | "bookmark-all": { 47 | "suggested_key": { 48 | "default": "Alt+Shift+W" 49 | }, 50 | "description": "Window tabs" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /webextension/popup/actions.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 4 | } 5 | 6 | html, body { 7 | margin: 0; 8 | padding: 3px 0; 9 | } 10 | 11 | .button { 12 | position: relative; 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | border: 0; 17 | width: 100%; 18 | height: 24px; 19 | color: #1A1A1A; 20 | font-size: 13px; 21 | padding: 0 16px; 22 | text-align: left; 23 | font-weight: 400; 24 | user-select: none; 25 | -moz-user-select: none; 26 | -ms-user-select: none; 27 | -webkit-user-select: none; 28 | } 29 | 30 | .button:hover { 31 | background-color: #EEEEEE; 32 | } 33 | 34 | .button:active { 35 | box-shadow: inset 0 1px #DFDFDF; 36 | background-color: #E6E6E6; 37 | } 38 | 39 | .text { 40 | flex-grow: 1; 41 | margin-right: 16px; 42 | } 43 | 44 | .shortcut { 45 | color: #7F7F7F; 46 | float: right; 47 | justify-content: flex-end; 48 | } 49 | 50 | table { 51 | position: relative; 52 | right: -5px; 53 | border-collapse: collapse; 54 | } 55 | 56 | table td { 57 | text-align: center; 58 | padding: 0; 59 | width: 14px; 60 | } 61 | 62 | td:last-child { 63 | padding-left: 2px; 64 | text-align: left; 65 | } 66 | -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webextension/background-script.js: -------------------------------------------------------------------------------- 1 | const baseURL = 'https://bmarks.net/add/'; 2 | 3 | // shortcuts 4 | browser.commands.onCommand.addListener(command => { 5 | callFunctionForCommand(command); 6 | }); 7 | 8 | // popup 9 | browser.runtime.onMessage.addListener(message => { 10 | callFunctionForCommand(message.command); 11 | }); 12 | 13 | function callFunctionForCommand(command) { 14 | switch (command) { 15 | case 'bookmark-current': 16 | bookmarkCurrentPage(); break; 17 | case 'bookmark-all': 18 | bookmarkAllTabsOnWindow(); break; 19 | } 20 | } 21 | 22 | function createTabAndSendMessage(message) { 23 | browser.tabs.create({url: baseURL}).then(marksTab => { 24 | browser.tabs.executeScript({code: ''}).then(() => { 25 | browser.tabs.sendMessage(marksTab.id, message); 26 | }); 27 | }); 28 | } 29 | 30 | function bookmarkCurrentPage() { 31 | browser.tabs.query({currentWindow: true, active: true}).then(tabs => { 32 | let title = tabs[0].title; 33 | let url = tabs[0].url; 34 | 35 | createTabAndSendMessage({command: 'insertCurrentTab', title: title, url: url}); 36 | }); 37 | } 38 | 39 | function bookmarkAllTabsOnWindow() { 40 | browser.tabs.query({currentWindow: true}).then(tabs => { 41 | let description = tabs.map(tab => `- [${ tab.title }](${ tab.url })`).join('\n'); 42 | 43 | createTabAndSendMessage({command: 'insertAllTabs', description: description}); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io and modified by Felipe 2 | 3 | # Marks 4 | src/static 5 | trash/ 6 | marksenv/ 7 | 8 | ### OSX ### 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | 13 | # Icon must end with two \r 14 | Icon 15 | 16 | 17 | # Thumbnails 18 | ._* 19 | 20 | # Files that might appear on external disk 21 | .Spotlight-V100 22 | .Trashes 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | ### Python ### 33 | # Byte-compiled / optimized / DLL files 34 | __pycache__/ 35 | *.py[cod] 36 | 37 | # C extensions 38 | *.so 39 | 40 | # Distribution / packaging 41 | .Python 42 | env/ 43 | build/ 44 | develop-eggs/ 45 | dist/ 46 | downloads/ 47 | eggs/ 48 | lib/ 49 | lib64/ 50 | parts/ 51 | sdist/ 52 | var/ 53 | *.egg-info/ 54 | .installed.cfg 55 | *.egg 56 | 57 | # PyInstaller 58 | # Usually these files are written by a python script from a template 59 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 60 | *.manifest 61 | *.spec 62 | 63 | # Installer logs 64 | pip-log.txt 65 | pip-delete-this-directory.txt 66 | 67 | # Unit test / coverage reports 68 | htmlcov/ 69 | .tox/ 70 | .coverage 71 | .cache 72 | nosetests.xml 73 | coverage.xml 74 | 75 | # Translations 76 | *.mo 77 | *.pot 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | 86 | ### Django ### 87 | *.log 88 | *.pot 89 | *.pyc 90 | __pycache__/ 91 | local_settings.py 92 | 93 | .env 94 | db.sqlite3 95 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0011_profile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2018-03-18 10:53 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ("marksapp", "0010_auto_20180127_0908"), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name="Profile", 20 | fields=[ 21 | ( 22 | "id", 23 | models.AutoField( 24 | auto_created=True, 25 | primary_key=True, 26 | serialize=False, 27 | verbose_name="ID", 28 | ), 29 | ), 30 | ( 31 | "visibility", 32 | models.CharField( 33 | choices=[("PB", "Public"), ("PV", "Private")], max_length=2 34 | ), 35 | ), 36 | ("email", models.CharField(blank=True, max_length=128)), 37 | ( 38 | "user", 39 | models.OneToOneField( 40 | on_delete=django.db.models.deletion.CASCADE, 41 | to=settings.AUTH_USER_MODEL, 42 | ), 43 | ), 44 | ], 45 | ) 46 | ] 47 | -------------------------------------------------------------------------------- /app/marksapp/models.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db import models 3 | from django.utils import timezone 4 | from django.contrib.auth.models import User 5 | from django.core.validators import validate_slug 6 | 7 | 8 | class Profile(models.Model): 9 | user = models.OneToOneField(User, on_delete=models.CASCADE) 10 | email = models.CharField(max_length=128, blank=True) 11 | 12 | PUBLIC = "PB" 13 | PRIVATE = "PV" 14 | visibility_choices = [(PUBLIC, "Public"), (PRIVATE, "Private")] 15 | visibility = models.CharField( 16 | choices=visibility_choices, max_length=2, default=PUBLIC 17 | ) 18 | 19 | def __str__(self): 20 | return "{} | {}".format(self.user.username, self.visibility) 21 | 22 | 23 | class Tag(models.Model): 24 | name = models.SlugField(max_length=50, unique=True) 25 | 26 | class Meta: 27 | ordering = ["name"] 28 | 29 | def __str__(self): 30 | return self.name 31 | 32 | 33 | class Bookmark(models.Model): 34 | name = models.CharField(max_length=128) 35 | url = models.CharField(max_length=512, blank=True) 36 | description = models.TextField(blank=True) 37 | date_added = models.DateTimeField("date added", default=timezone.now) 38 | last_bumped = models.DateTimeField("last bumped", blank=True, null=True) 39 | tags = models.ManyToManyField(Tag) 40 | user = models.ForeignKey(User, on_delete=models.CASCADE, default=1) 41 | 42 | def tags_str(self): 43 | return ", ".join(t.name for t in self.tags.all()) 44 | 45 | def __str__(self): 46 | return "[{}] - {} - {} - [{}] - <{}>".format( 47 | self.date_added, self.user.username, self.name, self.url, self.tags_str() 48 | ) 49 | -------------------------------------------------------------------------------- /app/marksapp/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load filters %} 3 | 4 | {% block page_info %} 5 | 6 | · 7 | {{ username }} 8 | · 9 | profile 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
15 |
16 | {% csrf_token %} 17 | {{ form.non_field_errors }} 18 | 19 | {{ user_form.email }} 20 | 21 | 22 | {% for radio in profile_form.visibility %} 23 |
24 | {{ radio }} 25 |
26 | {% endfor %} 27 | 28 | 29 |
30 | 31 |
32 | 33 |
34 | {% csrf_token %} 35 | 36 | {{ password_form.non_field_errors }} 37 | 38 | 39 | 40 | {{ password_form.old_password.errors }} 41 | 42 | 43 | {{ password_form.new_password1 }} 44 | {{ password_form.new_password1.errors }} 45 | 46 | 47 | {{ password_form.new_password2 }} 48 | {{ password_form.new_password2.errors }} 49 | 50 | 51 |
52 | 53 |
54 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /app/marksapp/management/commands/read_file.py: -------------------------------------------------------------------------------- 1 | from html.parser import HTMLParser 2 | from datetime import datetime 3 | 4 | 5 | class NetscapeParser(HTMLParser): 6 | add_mark = False 7 | add_cat = False 8 | add_date = 0 9 | icon = "" 10 | href = "" 11 | tags = "" 12 | categories = [] 13 | bookmarks = [] 14 | 15 | def handle_starttag(self, tag, attrs): 16 | if tag == "h3": 17 | self.add_cat = True 18 | if tag == "a": 19 | self.add_mark = True 20 | for attr in attrs: 21 | if attr[0] == "href": 22 | self.href = attr[1] 23 | elif attr[0] == "add_date": 24 | self.add_date = datetime.utcfromtimestamp(int(attr[1])) 25 | elif attr[0] == "icon": 26 | self.icon = attr[1] 27 | elif attr[0] == "tags": 28 | self.tags = attr[1].split(",") 29 | 30 | def handle_endtag(self, tag): 31 | if tag == "dl": 32 | if self.categories: 33 | self.categories.pop() 34 | 35 | def handle_data(self, data): 36 | if self.add_cat == True: 37 | self.categories.append(data.lower()) 38 | self.add_cat = False 39 | elif self.add_mark == True: 40 | mark = {} 41 | mark["name"] = data 42 | mark["url"] = self.href 43 | mark["categories"] = self.categories[:] 44 | mark["tags"] = self.tags 45 | mark["add_date"] = self.add_date 46 | self.bookmarks.append(mark) 47 | self.tags = "" 48 | self.add_mark = False 49 | 50 | 51 | def bookmarks_from_file(filename): 52 | with open(filename, "r") as f: 53 | bookmarks = f.read() 54 | 55 | parser = NetscapeParser() 56 | parser.feed(bookmarks) 57 | return parser.bookmarks 58 | -------------------------------------------------------------------------------- /app/marksapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-09 19:16 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="Bookmark", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("name", models.CharField(max_length=128)), 29 | ("url", models.CharField(max_length=512)), 30 | ( 31 | "date_added", 32 | models.DateTimeField( 33 | default=django.utils.timezone.now, verbose_name="date added" 34 | ), 35 | ), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name="Tag", 40 | fields=[ 41 | ( 42 | "id", 43 | models.AutoField( 44 | auto_created=True, 45 | primary_key=True, 46 | serialize=False, 47 | verbose_name="ID", 48 | ), 49 | ), 50 | ("name", models.SlugField(unique=True)), 51 | ], 52 | ), 53 | migrations.AddField( 54 | model_name="bookmark", 55 | name="tags", 56 | field=models.ManyToManyField(to="marksapp.Tag"), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /webextension/popup/actions.js: -------------------------------------------------------------------------------- 1 | const nameToUnicode = { 2 | 'Ctrl' : '⌘', 3 | 'Alt' : '⌥', 4 | 'Shift' : '⇧' 5 | } 6 | 7 | function shortcutToMac(shortcut) { 8 | return shortcut.split('+') 9 | .map(key => nameToUnicode[key] || key) 10 | .join(''); 11 | } 12 | 13 | function convertShortcutToSymbolsIfMac(shortcutElement) { 14 | browser.runtime.getPlatformInfo().then(info => { 15 | if (info.os == 'mac') { 16 | let rows = Array.from(shortcutToMac(shortcutElement.innerText), 17 | char => `${ char }`) 18 | .join(''); 19 | 20 | shortcutElement.innerHTML = `${ rows }
`; 21 | } 22 | }); 23 | } 24 | 25 | function createCommandElement(name, shortcut, command) { 26 | var aElement = document.createElement('a'); 27 | aElement.classList.add('button'); 28 | 29 | var textElement = document.createElement('span'); 30 | textElement.innerText = name; 31 | textElement.classList.add('text'); 32 | aElement.appendChild(textElement); 33 | 34 | if (shortcut !== null) { 35 | var shortcutElement = document.createElement('span'); 36 | shortcutElement.innerText = shortcut; 37 | shortcutElement.classList.add('shortcut'); 38 | convertShortcutToSymbolsIfMac(shortcutElement); 39 | aElement.appendChild(shortcutElement); 40 | } 41 | 42 | aElement.onclick = () => { 43 | browser.runtime.sendMessage({command: command}); 44 | window.close(); 45 | }; 46 | 47 | return aElement; 48 | } 49 | 50 | browser.commands.getAll().then(commands => { 51 | const commandsDiv = document.getElementById('commands'); 52 | 53 | commands.forEach(command => { 54 | let commandElement = createCommandElement(command.description, 55 | command.shortcut, 56 | command.name); 57 | 58 | commandsDiv.appendChild(commandElement); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /app/etc/changelog.markdown: -------------------------------------------------------------------------------- 1 | changelog 2 | --------- 3 | 4 | ## 2018-03-26 5 | - Added Firefox webextension 6 | 7 | ## 2018-03-20 8 | - Default visibility working 9 | 10 | ## 2018-03-16 11 | - Improved register form, added email and visibility fields 12 | 13 | ## 2018-03-13 14 | - More little CSS changes 15 | 16 | ## 2018-03-12 17 | - Fixed wrapping in descriptions 18 | - Changed Analytics script 19 | - Lots of little CSS changes 20 | 21 | ## 2018-03-11 22 | - Moved to bmarks.net 23 | - Writing a guide 24 | - Improved readme 25 | 26 | ## 2018-03-10 27 | - Nowrap for marks, scrolling container and proper descriptions 28 | - Description now hidden by default. Added buttons to expand/collapse 29 | 30 | ## 2018-03-09 31 | - Registration page 32 | 33 | ## 2018-01-13 34 | - Project organisation 35 | 36 | ## 2018-01-08 37 | - Suggestions now clickable 38 | 39 | ## 2017-07-27 40 | - Added logging 41 | 42 | ## 2017-05-07 43 | - Added "last" page 44 | - Added analytics js 45 | 46 | ## 2017-05-05 47 | - Tag suggestions work better 48 | - Javascript tag search works better 49 | 50 | ## 2017-05-04 51 | - Real-time tag search focus on first found result 52 | 53 | ## 2017-05-03 54 | - Index page real-time tag search using '/' 55 | 56 | ## 2017-05-02 57 | - Moved tag overlaps to the bottom in marks page 58 | - Sorting options (name, date added) 59 | - Doesn't work in search yet 60 | - Info alt text (date added) 61 | 62 | ## 2017-05-01 63 | - Bookmarklet! 64 | - javascript:location.href='https://bmarks.net/add/?url='+encodeURIComponent(location.href)+'&name='+encodeURIComponent(document.title) 65 | 66 | ## 2017-04-30 67 | - Bug fixes 68 | - Title automatically inserted from URL in add form 69 | 70 | ## 2017-04-29 71 | - Search and tag view is now the same 72 | - This fixes the 'all' page 73 | 74 | ## 2017-04-27 75 | - Added viewport tag which solves CSS sizing headaches 76 | - Automatic focus on mark editing 77 | - Search form working for every page 78 | 79 | ## 2017-04-26 80 | - Made deployment process easier 81 | - Deleted 'delete' button from tag list 82 | - Made a better page for all bookmarks 83 | - Fixed delete (changed to JSON) 84 | - Added a changelog! :) 85 | -------------------------------------------------------------------------------- /app/marksapp/templates/mark.html: -------------------------------------------------------------------------------- 1 | {% load filters %} 2 | 3 |
  • 4 | 5 | {% if mark.description %} 6 | [+] 7 | {% endif %} 8 | {% if mark.url %} 9 | {{ mark.name }} 10 | {% else %} 11 | {{ mark.name }} 12 | {% endif %} 13 | {% for tag in mark.tags.all %} 14 | 15 | {% if tag.name %} 16 | {% if not search and tag.name not in tags %} 17 | + 19 | {% endif %} 20 | {{ tag.name }} 21 | {% endif %} 22 | 23 | {% endfor %} 24 | · 25 | 26 | 27 |
    28 | {% if user.is_authenticated and user.username == username %} 29 | edit 30 | delete 31 | bump 32 | {% if mark.url %} 33 | wayback 34 | {% endif %} 35 | 36 | {% endif %} 37 | permalink 38 |
    39 | {% if mark.description %} 40 |
    41 |
    42 |
    43 | {{ mark.description|markdown|safe }} 44 |
    45 |
    46 | {% endif %} 47 |
  • 48 | -------------------------------------------------------------------------------- /app/marksapp/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
    5 | 6 |
    7 | 10 | 13 | 18 |
    19 | 20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 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 | 54 | 55 |
    56 |
    57 | 58 | 72 | 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /app/marksapp/netscape.py: -------------------------------------------------------------------------------- 1 | from django.utils.text import slugify 2 | from marksapp.models import Bookmark, Tag 3 | from html.parser import HTMLParser 4 | import datetime 5 | 6 | 7 | class NetscapeParser(HTMLParser): 8 | add_mark = False 9 | add_cat = False 10 | add_date = 0 11 | icon = "" 12 | href = "" 13 | tags = [] 14 | categories = [] 15 | bookmarks = [] 16 | 17 | def handle_starttag(self, tag, attrs): 18 | if tag == "h3": 19 | self.add_cat = True 20 | if tag == "a": 21 | self.add_mark = True 22 | for attr in attrs: 23 | if attr[0] == "href": 24 | self.href = attr[1] 25 | elif attr[0] == "add_date": 26 | self.add_date = datetime.datetime.utcfromtimestamp( 27 | int(attr[1]) 28 | ).replace(tzinfo=datetime.timezone.utc) 29 | elif attr[0] == "icon": 30 | self.icon = attr[1] 31 | elif attr[0] == "tags": 32 | self.tags = attr[1].split(",") 33 | 34 | def handle_endtag(self, tag): 35 | if tag == "dl": 36 | if self.categories: 37 | self.categories.pop() 38 | 39 | def handle_data(self, data): 40 | if self.add_cat == True: 41 | self.categories.append(data.lower()) 42 | self.add_cat = False 43 | elif self.add_mark == True: 44 | mark = {} 45 | mark["name"] = data 46 | mark["url"] = self.href 47 | mark["categories"] = self.categories[:] 48 | mark["tags"] = self.tags[:] 49 | mark["add_date"] = self.add_date 50 | self.bookmarks.append(mark) 51 | self.tags = [] 52 | self.add_mark = False 53 | 54 | 55 | def bookmarks_from_file(f, user): 56 | bookmarks = f.read() 57 | 58 | parser = NetscapeParser() 59 | parser.feed(bookmarks.decode("utf-8")) 60 | 61 | for mark in parser.bookmarks: 62 | b = Bookmark.objects.update_or_create(user=user, url=mark["url"][:512])[0] 63 | b.name = mark["name"][:128] 64 | b.date_added = mark["add_date"] 65 | 66 | for tag in mark["categories"] + mark["tags"]: 67 | t = Tag.objects.get_or_create(name=slugify(tag))[0] 68 | b.tags.add(t) 69 | 70 | b.save() 71 | -------------------------------------------------------------------------------- /app/marksapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path, re_path 2 | from marksapp.misc import multitag_regex 3 | 4 | from . import views 5 | 6 | urlpatterns = [ 7 | re_path(r"^$", views.index, name="index"), 8 | re_path(r"^add/$", views.add_mark, name="add_mark"), 9 | re_path(r"^mark/(?P[0-9]+)/delete/$", views.delete_mark, name="delete_mark"), 10 | # api 11 | re_path(r"^api/mark/(?P[0-9]+)/$", views.api_mark, name="api_mark"), 12 | re_path(r"^api/tags/(?P[-\w\d+]+)$", views.api_tags, name="api_tags"), 13 | re_path(r"^api/tags/", views.api_tags, name="api_tags"), 14 | re_path(r"^api/get_title/$", views.api_get_title, name="api_get_title"), 15 | re_path( 16 | r"^api/delete_mark/(?P[0-9]+)/$", 17 | views.api_delete_mark, 18 | name="api_delete_mark", 19 | ), 20 | re_path(r"^api/delete_marks/$", views.api_delete_marks, name="api_delete_marks"), 21 | re_path(r"^api/edit_multiple/$", views.api_edit_multiple, name="api_edit_multiple"), 22 | re_path( 23 | r"^api/bump_mark/(?P[0-9]+)/$", views.api_bump_mark, name="api_bump_mark" 24 | ), 25 | re_path( 26 | r"^block/mark/(?P[0-9]+)/$", views.edit_mark_form, name="edit_mark_form" 27 | ), 28 | # user views 29 | re_path( 30 | r"^(?P\w+)/mark/(?P[0-9]+)/$", 31 | views.mark_permalink, 32 | name="mark_permalink", 33 | ), 34 | re_path( 35 | r"^(?P\w+)/tag/(?P{})/$".format(multitag_regex[1:-1]), 36 | views.user_tag, 37 | name="user_tag", 38 | ), 39 | re_path(r"^(?P\w+)/tag/$", views.user_tag, name="user_tag"), 40 | re_path(r"^(?P\w+)$", views.user_index, name="user_index"), 41 | re_path(r"^profile/$", views.user_profile, name="user_profile"), 42 | re_path(r"^import_netscape/$", views.import_netscape, name="import_netscape"), 43 | re_path(r"^import_json/$", views.import_json, name="import_json"), 44 | re_path(r"^export_json/$", views.export_json, name="export_json"), 45 | re_path(r"^changelog/$", views.changelog, name="changelog"), 46 | re_path(r"^register/$", views.register, name="register"), 47 | re_path(r"^guide/$", views.guide, name="guide"), 48 | re_path(r"^csv/$", views.csv_export, name="csv"), 49 | re_path( 50 | r"^", 51 | include("django.contrib.auth.urls"), 52 | {"extra_context": {"page_title": "login"}}, 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /app/marksapp/templates/guide.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_info %} 4 | 5 | · 6 | guide 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |

    Importing

    13 |

    You can import your bookmarks from Chrome, Firefox, Safari, Opera, Internet Explorer, as long as they're in the Netscape bookmark format. Your directories will be converted to tags automatically. If you have a bookmark inside the folders "Photography -> Music" it'll get the tags "Photography" and "Music". If you have a bookmark inside the folders "Music -> Photography", it will get the tags "Photography" and "Music" as well.

    14 |

    Adding bookmarks

    15 |

    URL

    16 |

    That's the link to your bookmarked site. If you copy and paste it, the title will be fetched from the page automatically and added to the Name field as long as it is blank.

    17 |

    Name

    18 | The name of your bookmark. 19 |

    Tags

    20 |

    Comma or space separated tags for your bookmark. "music, songwriting, people" tags a bookmark as "music", "songwriting" and "people". So does "music songwriting people". So does "music, songwriting people"!

    21 |

    Descriptions

    22 |

    When you add a bookmark, you can write a description for it using Markdown syntax.

    23 |

    Viewing bookmarks

    24 |

    You can sort bookmark listings by name or by date (most recent first). 25 | The description for a bookmark is hidden by default and can be seen by clicking on [+]. Clicking expand all will expand all descriptions, and collapse all will close all of them.

    26 |

    Privacy

    27 |

    Public profiles

    28 |

    If you set your profile to public, everyone can see your bookmarks unless they're tagged as "private".

    29 |

    Unlisted tags

    30 |

    If you prefix a tag with a dot, like ".notsosecret", bookmarks tagged with it won't show up if another user's browsing your tags or bookmarks, but you can still share links to that tag.

    31 |

    Private profiles

    32 |

    If you set your profile to private, nobody can see your bookmarks unless they're tagged as "public".

    33 |

    Contact / Contribute

    34 |

    If you need anything or have a suggestion just send an email to bmarks@felipecortez.net. bmarks is open-source software built with Python and Django. You can find all the code, suggest changes and contribute at GitHub.

    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/marksapp/tests/test_misc.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from marksapp.models import Tag, Bookmark 4 | from marksapp.misc import tag_regex, multitag_regex, website_from_url 5 | import marksapp.views as views 6 | import re 7 | 8 | 9 | class WebsiteFromURL(TestCase): 10 | @classmethod 11 | def setUpTestData(cls): 12 | Tag.objects.create(name="music") 13 | Tag.objects.create(name="compsci") 14 | 15 | def test_extract(self): 16 | strs_to_test = { 17 | "https://bmarks.net": "bmarks.net", 18 | "http://bmarks.net": "bmarks.net", 19 | "ftp://bmarks.net": "bmarks.net", 20 | "bmarks.net": "bmarks.net", 21 | "www.bmarks.net": "bmarks.net", 22 | "www.bmarks.net/": "bmarks.net", 23 | "www.bmarks.net/etc": "bmarks.net", 24 | # "subdomain.bmarks.net/etc" : "bmarks.net", 25 | # "iamnotadomain" : None, 26 | } 27 | 28 | for url, expected in strs_to_test.items(): 29 | self.assertEquals(expected, website_from_url(url)) 30 | 31 | 32 | class TagSplitTests(TestCase): 33 | @classmethod 34 | def setUpTestData(cls): 35 | Tag.objects.create(name="music") 36 | Tag.objects.create(name="compsci") 37 | 38 | def test_creation(self): 39 | music_tag = Tag.objects.get(id=1) 40 | self.assertEquals(music_tag.name, "music") 41 | 42 | def test_split(self): 43 | strs_to_test = [ 44 | "music, compsci, art", 45 | " music, compsci art", 46 | "music, compsci,art ", 47 | " music,compsci, art", 48 | " music, compsci,,,art", 49 | "music, compsci, art", 50 | "music compsci art", 51 | ] 52 | 53 | for string in strs_to_test: 54 | self.assertEquals( 55 | views.tags_strip_split(string), ["music", "compsci", "art"] 56 | ) 57 | 58 | 59 | class RegexTests(TestCase): 60 | @classmethod 61 | def setUpTestData(cls): 62 | pass 63 | 64 | def test_simple(self): 65 | tag_simple = ".unlisted" 66 | assert re.match(tag_regex, tag_simple) is not None 67 | 68 | def test_multi(self): 69 | multi_tags = [ 70 | "compsci", 71 | ".unlisted", 72 | ".unlisted+compsci", 73 | ".unlisted+.notreallylisted", 74 | "very+normal+indeed", 75 | ] 76 | 77 | for string in multi_tags: 78 | assert re.match(multitag_regex, string) is not None 79 | -------------------------------------------------------------------------------- /app/marksapp/tests/test_pagination.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from marksapp.models import Tag, Bookmark 4 | import marksapp.views as views 5 | 6 | 7 | def marks_names(marks): 8 | return [m.name for m in marks] 9 | 10 | 11 | class PaginateTests(TestCase): 12 | @classmethod 13 | def setUpTestData(cls): 14 | User.objects.create_user(username="testuser") 15 | 16 | for i in range(1, 12): 17 | Bookmark.objects.create(name=f"b{i}", date_added="2018-11-02T00:00+0000") 18 | 19 | def test_first_page(self): 20 | result = views.paginate(Bookmark.objects, limit=3) 21 | self.assertEqual(marks_names(result["marks"]), ["b11", "b10", "b9"]) 22 | self.assertEqual(result["after_link"].name, "b9") 23 | self.assertIs(result["before_link"], None) 24 | 25 | def test_transition(self): 26 | first_page = views.paginate(Bookmark.objects, limit=3) 27 | after_first = first_page["after_link"] 28 | second_page = views.paginate(Bookmark.objects, after=after_first, limit=3) 29 | 30 | self.assertEqual(marks_names(second_page["marks"]), ["b8", "b7", "b6"]) 31 | self.assertEqual(second_page["after_link"].name, "b6") 32 | self.assertEqual(second_page["before_link"].name, "b8") 33 | 34 | def test_after(self): 35 | b7 = Bookmark.objects.get(name="b7") 36 | result = views.paginate(Bookmark.objects, after=b7, limit=3) 37 | 38 | self.assertEqual(marks_names(result["marks"]), ["b6", "b5", "b4"]) 39 | self.assertEqual(result["after_link"].name, "b4") 40 | self.assertEqual(result["before_link"].name, "b6") 41 | 42 | def test_before(self): 43 | b6 = Bookmark.objects.get(name="b6") 44 | result = views.paginate(Bookmark.objects, before=b6, limit=3) 45 | 46 | self.assertEqual(marks_names(result["marks"]), ["b9", "b8", "b7"]) 47 | self.assertEqual(result["after_link"].name, "b7") 48 | self.assertEqual(result["before_link"].name, "b9") 49 | 50 | def test_incomplete_last_page(self): 51 | b3 = Bookmark.objects.get(name="b3") 52 | result = views.paginate(Bookmark.objects, after=b3, limit=3) 53 | 54 | self.assertEqual(marks_names(result["marks"]), ["b2", "b1"]) 55 | self.assertIs(result["after_link"], None) 56 | self.assertEqual(result["before_link"].name, "b2") 57 | 58 | def test_pagination_by_name(self): 59 | result = views.paginate(Bookmark.objects, limit=5, sort_column="name") 60 | 61 | self.assertEqual(marks_names(result["marks"]), ["b1", "b10", "b11", "b2", "b3"]) 62 | self.assertEqual(result["after_link"].name, "b3") 63 | self.assertIs(result["before_link"], None) 64 | 65 | def test_pagination_by_name_inverse(self): 66 | result = views.paginate(Bookmark.objects, limit=5, sort_column="-name") 67 | 68 | self.assertEqual(marks_names(result["marks"]), ["b9", "b8", "b7", "b6", "b5"]) 69 | self.assertEqual(result["after_link"].name, "b5") 70 | self.assertIs(result["before_link"], None) 71 | -------------------------------------------------------------------------------- /app/marksapp/management/commands/setupfromfile.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.utils.text import slugify 3 | from marksapp.models import Bookmark, Tag 4 | from html.parser import HTMLParser 5 | import datetime 6 | 7 | 8 | class NetscapeParser(HTMLParser): 9 | add_mark = False 10 | add_cat = False 11 | add_date = 0 12 | icon = "" 13 | href = "" 14 | tags = [] 15 | categories = [] 16 | bookmarks = [] 17 | 18 | def handle_starttag(self, tag, attrs): 19 | if tag == "h3": 20 | self.add_cat = True 21 | if tag == "a": 22 | self.add_mark = True 23 | for attr in attrs: 24 | if attr[0] == "href": 25 | self.href = attr[1] 26 | elif attr[0] == "add_date": 27 | self.add_date = datetime.datetime.utcfromtimestamp( 28 | int(attr[1]) 29 | ).replace(tzinfo=datetime.timezone.utc) 30 | elif attr[0] == "icon": 31 | self.icon = attr[1] 32 | elif attr[0] == "tags": 33 | self.tags = attr[1].split(",") 34 | 35 | def handle_endtag(self, tag): 36 | if tag == "dl": 37 | if self.categories: 38 | self.categories.pop() 39 | 40 | def handle_data(self, data): 41 | if self.add_cat == True: 42 | self.categories.append(data.lower()) 43 | self.add_cat = False 44 | elif self.add_mark == True: 45 | mark = {} 46 | mark["name"] = data 47 | mark["url"] = self.href 48 | mark["categories"] = self.categories[:] 49 | mark["tags"] = self.tags[:] 50 | mark["add_date"] = self.add_date 51 | self.bookmarks.append(mark) 52 | self.tags = [] 53 | self.add_mark = False 54 | 55 | 56 | def bookmarks_from_file(filename): 57 | with open(filename, "r") as f: 58 | bookmarks = f.read() 59 | 60 | parser = NetscapeParser() 61 | parser.feed(bookmarks) 62 | return parser.bookmarks 63 | 64 | 65 | class Command(BaseCommand): 66 | help = "Populate DB from a Netscape bookmark file" 67 | 68 | def add_arguments(self, parser): 69 | parser.add_argument("filename") 70 | 71 | def handle(self, *args, **options): 72 | Bookmark.objects.all().delete() 73 | Tag.objects.all().delete() 74 | 75 | for mark in bookmarks_from_file(options["filename"]): 76 | b = Bookmark.objects.update_or_create(url=mark["url"])[0] 77 | b.name = mark["name"] 78 | b.date_added = mark["add_date"] 79 | 80 | for tag in mark["categories"] + mark["tags"]: 81 | t = Tag.objects.get_or_create(name=slugify(tag))[0] 82 | b.tags.add(t) 83 | 84 | b.save() 85 | 86 | print(Bookmark.objects.all()) 87 | 88 | # b1 = Bookmark(name="Opa", 89 | # url="opa.com") 90 | # b2 = Bookmark(name="Bicho", 91 | # url="bicho.com") 92 | # b2.save() 93 | 94 | # t1 = Tag(name="inutil") 95 | # t2 = Tag(name="util") 96 | # t3 = Tag(name="teste") 97 | # b1.save() 98 | # t1.save() 99 | # t2.save() 100 | # t3.save() 101 | # b1.tags.add(t1) 102 | # b1.save() 103 | # b2.tags.add(t2) 104 | # b2.tags.add(t3) 105 | # b2.save() 106 | 107 | # print(b1) 108 | # print(b2) 109 | -------------------------------------------------------------------------------- /app/marksapp/templates/marks.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load filters %} 3 | 4 | {% block page_info %} 5 | 6 | · 7 | {{ username }} 8 | {% if params.search_title %} 9 | · 10 | {{ params.search_title }}{% endif %} 11 | {% if tags %} 12 | · 13 | {% endif %} 14 | 15 | {% for tag in tags %} 16 | 17 | {% if tags|length > 1 %} 18 | x 20 | {% else %} 21 | x 23 | {% endif %} 24 | {{ tag }} 25 | 26 | {% if not forloop.last %} 27 | & 28 | {% endif %} 29 | {% endfor %} 30 | {% endblock %} 31 | 32 | {% block content %} 33 | 34 | {% if bookmarks %} 35 |
    36 |
    37 | 42 | 45 | 48 | 51 |
    52 | 53 |
    54 | 55 |
    56 |
    57 | {% csrf_token %} 58 | 59 | 60 | 61 | 62 | 63 |
    64 | 65 |
    66 | 69 | 72 | 75 | 78 |
    79 |
    80 | 81 | 86 | 87 |
    88 | {% if before_mark or after_mark %} 89 | {% if before_mark %} 90 | 91 | {% endif %} 92 | 93 | {% if before_mark and after_mark %} 94 | · 95 | {% endif %} 96 | 97 | {% if after_mark %} 98 | 99 | {% endif %} 100 | {% else %}that's all{% endif %} 101 |
    102 | 103 | 104 |
      105 | {% for tag in tag_count %} 106 | {% if tag.name and tag.name not in tags %} 107 |
    • 108 | 109 | + 111 | {{ tag.name }} 113 | 114 | {{ tag.num_marks }} 115 |
    • 116 | {% endif %} 117 | {% endfor %} 118 |
    119 | {% else %} 120 |

    No results found.

    121 | {% endif %} 122 | 123 | {% endblock %} 124 | -------------------------------------------------------------------------------- /app/marks/settings.py: -------------------------------------------------------------------------------- 1 | from .config import * 2 | import os 3 | import logging 4 | 5 | SECRET_KEY = "defaultkey" 6 | 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | 9 | if os.environ.get("DJANGO_DEVELOPMENT") is not None: 10 | DEBUG = True 11 | else: 12 | DEBUG = False 13 | 14 | ALLOWED_HOSTS = ["127.0.0.1", ".bmarks.net"] 15 | 16 | INTERNAL_IPS = ["127.0.0.1",] 17 | 18 | AUTHENTICATION_BACKENDS = ["marksapp.backends.CaseInsensitiveModelBackend"] 19 | 20 | INSTALLED_APPS = [ 21 | "marksapp.apps.MarksappConfig", 22 | "django.contrib.admin", 23 | "django.contrib.auth", 24 | "django.contrib.contenttypes", 25 | "django.contrib.sessions", 26 | "django.contrib.messages", 27 | "django.contrib.staticfiles", 28 | "django.contrib.postgres", 29 | "debug_toolbar", 30 | ] 31 | 32 | MIDDLEWARE = [ 33 | "django.middleware.security.SecurityMiddleware", 34 | "django.contrib.sessions.middleware.SessionMiddleware", 35 | "django.middleware.common.CommonMiddleware", 36 | "django.middleware.csrf.CsrfViewMiddleware", 37 | "django.contrib.auth.middleware.AuthenticationMiddleware", 38 | "django.contrib.messages.middleware.MessageMiddleware", 39 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 40 | "debug_toolbar.middleware.DebugToolbarMiddleware", 41 | ] 42 | 43 | ROOT_URLCONF = "marks.urls" 44 | 45 | TEMPLATES = [ 46 | { 47 | "BACKEND": "django.template.backends.django.DjangoTemplates", 48 | "DIRS": [], 49 | "APP_DIRS": True, 50 | "OPTIONS": { 51 | "context_processors": [ 52 | "django.template.context_processors.debug", 53 | "django.template.context_processors.request", 54 | "django.contrib.auth.context_processors.auth", 55 | "django.contrib.messages.context_processors.messages", 56 | ] 57 | }, 58 | } 59 | ] 60 | 61 | WSGI_APPLICATION = "marks.wsgi.application" 62 | 63 | 64 | if "TRAVIS" in os.environ: 65 | print("Using Travis CI for testing") 66 | 67 | DATABASES = { 68 | "default": { 69 | "ENGINE": "django.db.backends.postgresql", 70 | "NAME": "test_db", 71 | "USER": "postgres", 72 | "PASSWORD": "", 73 | "HOST": "localhost", 74 | "PORT": "", 75 | } 76 | } 77 | else: 78 | DATABASES = { 79 | "default": { 80 | "ENGINE": "django.db.backends.postgresql", 81 | "NAME": DB_NAME, 82 | "USER": DB_USER, 83 | "PASSWORD": DB_PW, 84 | "HOST": "db", 85 | "PORT": DB_PORT, 86 | } 87 | } 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 92 | }, 93 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 94 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 95 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 96 | ] 97 | 98 | LANGUAGE_CODE = "en-us" 99 | 100 | TIME_ZONE = "America/Recife" 101 | 102 | USE_I18N = True 103 | 104 | USE_L10N = True 105 | 106 | USE_TZ = True 107 | 108 | LOGIN_URL = "/login/" 109 | 110 | LOGIN_REDIRECT_URL = "/" 111 | 112 | LOGOUT_REDIRECT_URL = "/" 113 | 114 | STATIC_ROOT = ( 115 | "/srv/www/marks/static/" if not DEBUG else os.path.join(BASE_DIR, "static") 116 | ) 117 | 118 | STATIC_URL = "/static/" 119 | 120 | X_FRAME_OPTIONS = "DENY" 121 | 122 | SECURE_CONTENT_TYPE_NOSNIFF = not DEBUG 123 | 124 | SECURE_BROWSER_XSS_FILTER = not DEBUG 125 | 126 | SECURE_SSL_REDIRECT = False 127 | 128 | # SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTOCOL", "https") 129 | 130 | SESSION_COOKIE_SECURE = not DEBUG 131 | 132 | CSRF_COOKIE_SECURE = not DEBUG 133 | 134 | CSRF_COOKIE_HTTPONLY = not DEBUG 135 | 136 | LOGGING = { 137 | "version": 1, 138 | "disable_existing_loggers": False, 139 | "handlers": {"console": {"class": "logging.StreamHandler"}}, 140 | "loggers": { 141 | "django": { 142 | "handlers": ["console"], 143 | "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), 144 | } 145 | }, 146 | } 147 | 148 | DEBUG_TOOLBAR_CONFIG = { 149 | 'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG, 150 | } 151 | -------------------------------------------------------------------------------- /app/marksapp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms import ModelForm, CharField, ChoiceField 3 | from django.forms.widgets import TextInput 4 | from django.contrib.auth.models import User 5 | from marksapp.models import Bookmark, Tag, Profile 6 | from marksapp.misc import tag_regex 7 | import marksapp.views 8 | import re 9 | 10 | EMAIL_PLACEHOLDER_STR = "optional! just in case you forget your password" 11 | 12 | 13 | # https://github.com/wagtail/wagtail/issues/130#issuecomment-37180123 14 | # fucking colons... 15 | # hey maybe https://experiencehq.net/blog/better-django-modelform-html for placeholders 16 | class BaseForm(forms.Form): 17 | def __init__(self, *args, **kwargs): 18 | kwargs.setdefault( 19 | "label_suffix", "" 20 | ) # globally override the Django >=1.6 default of ':' 21 | super(BaseForm, self).__init__(*args, **kwargs) 22 | 23 | for field_name in self.fields: 24 | field = self.fields.get(field_name) 25 | if field: 26 | field.widget.attrs.update( 27 | { 28 | #'placeholder': field.label 29 | } 30 | ) 31 | 32 | 33 | class BaseModelForm(forms.ModelForm): 34 | def __init__(self, *args, **kwargs): 35 | kwargs.setdefault( 36 | "label_suffix", "" 37 | ) # globally override the Django >=1.6 default of ':' 38 | super(BaseModelForm, self).__init__(*args, **kwargs) 39 | 40 | for field_name in self.fields: 41 | field = self.fields.get(field_name) 42 | if field: 43 | field.widget.attrs.update( 44 | { 45 | #'placeholder': field.label 46 | } 47 | ) 48 | 49 | 50 | class CommaTags(TextInput): 51 | template_name = "comma_tags.html" 52 | 53 | def get_context(self, name, value, attrs): 54 | if value: 55 | objects = [] 56 | value = ", ".join([tag.name for tag in value]) 57 | 58 | context = super(TextInput, self).get_context(name, value, attrs) 59 | return context 60 | 61 | 62 | class BookmarkForm(BaseModelForm): 63 | tags = CharField(widget=CommaTags) 64 | 65 | class Meta: 66 | model = Bookmark 67 | fields = ["url", "name", "tags", "description"] 68 | widgets = { 69 | "description": forms.Textarea(attrs={"rows": 3, "placeholder": "optional"}) 70 | } 71 | 72 | def save(self, commit=True, *args, **kwargs): 73 | m = super(BookmarkForm, self).save(commit=False, *args, **kwargs) 74 | form_tags = marksapp.views.tags_strip_split(self.cleaned_data["tags"]) 75 | m.save() 76 | self.instance.tags.clear() 77 | 78 | for tag in form_tags: 79 | if re.match(tag_regex, tag): 80 | t = Tag.objects.get_or_create(name=tag)[0] 81 | m.tags.add(t) 82 | else: 83 | print("WRONG") 84 | print(self.instance.tags) 85 | return m 86 | 87 | 88 | class TagForm(BaseModelForm): 89 | class Meta: 90 | model = Tag 91 | fields = ["name"] 92 | 93 | def is_valid(self): 94 | valid = super(TagForm, self).is_valid() 95 | 96 | if not valid: 97 | for f_name in self.errors: 98 | print(f_name) 99 | return valid 100 | 101 | return True 102 | 103 | 104 | class RegistrationForm(BaseForm): 105 | username = CharField(label="Username", required=True) 106 | password = CharField(label="Password", required=True, widget=forms.PasswordInput) 107 | email = CharField( 108 | label="E-mail", 109 | required=False, 110 | widget=forms.TextInput(attrs={"placeholder": EMAIL_PLACEHOLDER_STR}), 111 | ) 112 | visibility = ChoiceField( 113 | choices=Profile.visibility_choices, 114 | label="Default visibility", 115 | initial="PB", 116 | widget=forms.RadioSelect(), 117 | ) 118 | 119 | 120 | class ProfileForm(BaseModelForm): 121 | class Meta: 122 | model = Profile 123 | fields = ["visibility"] 124 | widgets = {"visibility": forms.RadioSelect()} 125 | 126 | 127 | class UserForm(BaseModelForm): 128 | class Meta: 129 | model = User 130 | fields = ["email"] 131 | widgets = { 132 | "email": forms.TextInput(attrs={"placeholder": EMAIL_PLACEHOLDER_STR}) 133 | } 134 | 135 | 136 | class NetscapeForm(forms.Form): 137 | file = forms.FileField() 138 | 139 | 140 | class ImportJsonForm(forms.Form): 141 | file = forms.FileField() 142 | -------------------------------------------------------------------------------- /app/marksapp/templates/base.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | 3 | 4 | 5 | 6 | 7 | bmarks 8 | {% load static %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 31 | 77 | 78 |
    79 | 80 |
    81 | 82 | bmarks 83 | 84 | {% block page_info %} 85 | 86 | · 87 | {% if user.is_authenticated %} 88 | {% if user.username == username %} 89 | you 90 | {% else %} 91 | {{ username }} 92 | {% endif %} 93 | {% else %} 94 | {{ username }} 95 | {% endif %} 96 | {% endblock %} 97 | 98 |
    99 | 100 | {% block content %} 101 | {% endblock %} 102 |
    103 | 104 | 111 | 112 | 113 | 114 | 119 | 120 | 121 | 122 | 123 | {% endspaceless %} 124 | -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #F5F5F5; 3 | padding-top: 0; 4 | margin: 0; 5 | font-family: 'ff-real-headline-pro', Helvetica, Arial, sans-serif; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .container { 11 | background-color: white; 12 | padding: 15px; 13 | } 14 | 15 | /* navbar ------------------------------------- */ 16 | 17 | #navbar { 18 | position: relative; 19 | top: 0; 20 | left: 0; 21 | padding: 5px 0 5px 0; 22 | } 23 | 24 | #navbar ul { 25 | margin-left: 15px; 26 | } 27 | 28 | #navbar li { 29 | display: inline-block; 30 | font-size: 16px; 31 | color: #7F7F7F; 32 | margin-right: 20px; 33 | } 34 | 35 | #navbar a { 36 | color: #7F7F7F; 37 | } 38 | 39 | .active { 40 | text-decoration: underline; 41 | } 42 | 43 | /* extra nav ---------------------------------- */ 44 | 45 | .extra-nav { 46 | border-top: 1px solid #D8D8D8; 47 | border-bottom: 1px solid #D8D8D8; 48 | margin-bottom: 15px; 49 | padding-top: 5px; 50 | padding-bottom: 5px; 51 | max-width: 535px; 52 | } 53 | 54 | .extra-nav a { 55 | display: inline-block; 56 | color: #3F91CC; 57 | text-decoration: none; 58 | } 59 | 60 | .extra-nav .nav-item { 61 | display: inline-block; 62 | } 63 | 64 | .nav-item span { 65 | color: #7f7f76; 66 | } 67 | 68 | .separator { 69 | color: #3F91CC; 70 | user-select: none; 71 | -moz-user-select: none; 72 | -webkit-user-select: none; 73 | -ms-user-select: none; 74 | } 75 | 76 | .extra-nav .separator { 77 | margin-left: 5px; 78 | margin-right: 5px; 79 | } 80 | 81 | .item .separator { 82 | margin-left: 3px; 83 | margin-right: 3px; 84 | } 85 | 86 | #tags-flex { 87 | display: flex; 88 | flex-wrap: wrap; 89 | justify-content: space-between; 90 | max-width: 535px; 91 | } 92 | 93 | #marks .item { 94 | display: inline-block; 95 | } 96 | 97 | /* page title --------------------------------- */ 98 | 99 | #page-info, 100 | #page-info a { 101 | margin-top: 0; 102 | padding-top: 0; 103 | font-size: 24px; 104 | margin-bottom: 15px; 105 | text-decoration: none; 106 | } 107 | 108 | .just-green, 109 | .just-green a { 110 | font-weight: 300; 111 | color: #3cb568; 112 | } 113 | 114 | .italic { 115 | font-style: italic; 116 | } 117 | 118 | .green-and-bold, 119 | .green-and-bold a { 120 | font-weight: 400; 121 | color: #3cb568; 122 | } 123 | 124 | .search_title { 125 | color: #3f91cc; 126 | } 127 | 128 | .plus { 129 | padding: 0 5px 0 5px; 130 | margin-right: 5px; 131 | color: #7f7f76; 132 | } 133 | 134 | /* form --------------------------------------- */ 135 | 136 | .form-container { 137 | position: relative; 138 | padding: 15px; 139 | margin: 15px 0 15px 0; 140 | max-width: 500px; 141 | background-color: #F5F5F5; 142 | border: 2px solid #F5F5F5; 143 | border-radius: 2px; 144 | } 145 | 146 | .form-container::after { 147 | clear: both; 148 | content: ""; 149 | display: block; 150 | } 151 | 152 | .errorlist { 153 | margin-bottom: 10px; 154 | } 155 | 156 | label { 157 | display: block; 158 | max-width: 500px; 159 | font-size: 16px; 160 | color: #7F7F7F; 161 | margin-bottom: 10px; 162 | } 163 | 164 | input[type='text'], 165 | input[type=password], 166 | input[type=email], 167 | input[type=file], 168 | textarea { 169 | box-sizing: border-box; 170 | font-family: 'ff-real-headline-pro', Helvetica, Arial, sans-serif; 171 | background-color: #FEFEFE; 172 | border: 1px solid #979797; 173 | border-radius: 2px; 174 | font-size: 14px; 175 | color: #959595; 176 | width: 100%; 177 | padding: 10px; 178 | box-shadow: inset 2px 2px 0 rgba(0, 0, 0, .04), 2px 2px 0 rgba(0, 0, 0, .04); 179 | outline: none; 180 | margin-bottom: 15px; 181 | } 182 | 183 | input[type='text']:focus, 184 | input[type=password]:focus, 185 | input[type=email]:focus, 186 | input[type=file]:focus, 187 | input[type=submit]:focus, 188 | button:focus, 189 | textarea:focus { 190 | box-shadow: 0 0 5px rgba(63, 145, 204, .8); 191 | border-color: #1A79AA; 192 | } 193 | 194 | .edit_checkbox { 195 | display: none; 196 | } 197 | 198 | .edit_multiple_form { 199 | display: none; 200 | } 201 | 202 | .selected_actions { 203 | display: none; 204 | } 205 | 206 | input[type=checkbox] { 207 | margin: 0 5px 0 0; 208 | } 209 | 210 | .radio-buttons label { 211 | font-size: 14px; 212 | } 213 | 214 | textarea { 215 | min-height: 17px; 216 | resize: vertical; 217 | } 218 | 219 | input[type=submit], 220 | button { 221 | position: relative; 222 | display: block; 223 | font-family: 'ff-real-headline-pro', Helvetica, Arial, sans-serif; 224 | background-color: #FEFEFE; 225 | border: 1px solid #979797; 226 | border-radius: 2px; 227 | font-size: 14px; 228 | color: #959595; 229 | padding: 10px; 230 | box-shadow: 2px 2px 0 rgba(0, 0, 0, .04); 231 | outline: none; 232 | } 233 | 234 | input[type=submit]:hover, 235 | button:hover { 236 | background-color: #3F91CC; 237 | color: #FFFFFF; 238 | border-color: #1A79AA; 239 | } 240 | 241 | ::placeholder { 242 | color: #AAAAAA; 243 | font-style: italic; 244 | } 245 | 246 | .errorlist { 247 | background-color: rgb(255, 245, 153); 248 | color: rgb(100, 100, 38); 249 | padding: 8px; 250 | } 251 | 252 | /* main site ---------------------------------- */ 253 | 254 | .links { 255 | margin-bottom: 15px; 256 | overflow: auto; 257 | } 258 | 259 | .marginbottom { 260 | margin-bottom: 20px; 261 | } 262 | 263 | ul, ol { 264 | list-style-type: none; 265 | margin: 0; 266 | padding: 0; 267 | } 268 | 269 | li, li > a, li span { 270 | font-size: 14px; 271 | font-weight: 300; 272 | text-decoration: none; 273 | } 274 | 275 | li > a { 276 | color: #3F91CC; 277 | } 278 | 279 | .item { 280 | margin-bottom: 5px; 281 | white-space: nowrap; 282 | color: #333333; 283 | } 284 | 285 | .item > a, .item > .number, .item > .action, .item > span { 286 | margin-right: 5px; 287 | } 288 | 289 | .item > a:hover { 290 | border-bottom: 1px solid #36b2e8; 291 | } 292 | 293 | .tag { 294 | position: relative; 295 | display: inline-block; 296 | margin-right: 5px; 297 | font-size: 14px; 298 | white-space: nowrap; 299 | } 300 | 301 | .tag a { 302 | color: #7f7f76; 303 | text-decoration: none; 304 | padding: 0 5px 0 5px; 305 | background-color: #F5F5F5; 306 | font-weight: 300; 307 | } 308 | 309 | .tag a:hover { 310 | background-color: #DDDDDD; 311 | } 312 | 313 | .actions { 314 | display: none; 315 | } 316 | 317 | .action { 318 | margin-right: 3px; 319 | } 320 | 321 | .failed-action { 322 | color: #c08b8b !important; 323 | pointer-events: none; 324 | cursor: default; 325 | } 326 | 327 | .inline { 328 | display: inline; 329 | } 330 | 331 | .show-actions-checkbox { 332 | display: none; 333 | } 334 | 335 | .show-actions-checkbox:checked { 336 | display: none; 337 | } 338 | 339 | .show-actions-checkbox:checked ~ .actions { 340 | display: inline; 341 | } 342 | 343 | .actions-btn:hover { 344 | text-decoration: underline; 345 | cursor: pointer; 346 | } 347 | 348 | .action, .action a { 349 | color: #8badc0; 350 | padding: 0 1px 0 1px; 351 | text-decoration: none; 352 | } 353 | 354 | .action a:hover { 355 | text-decoration: underline; 356 | } 357 | 358 | .tag > .action { 359 | padding: 0 3px 0 3px; 360 | margin-right: 0; 361 | border-right: 1px solid #d8d7d2; 362 | } 363 | 364 | .action:hover { 365 | 366 | } 367 | 368 | .mdot { 369 | margin: 0 3px 0 3px; 370 | user-select: none; 371 | -moz-user-select: none; 372 | -webkit-user-select: none; 373 | -ms-user-select: none; 374 | } 375 | 376 | .big { 377 | font-size: 24px; 378 | padding: 5px 0 5px 0 !important; 379 | } 380 | 381 | .big a { 382 | padding: 5px 15px 5px 15px !important; 383 | 384 | } 385 | 386 | .number { 387 | color: #B2B2B2; 388 | font-size: 12px; 389 | font-weight: 300; 390 | } 391 | 392 | /* suggestions -------------------------------- */ 393 | 394 | #suggestions { 395 | position: absolute; 396 | border: 1px solid gray; 397 | box-shadow: inset 2px 2px 0 rgba(0, 0, 0, .04), 2px 2px 0 rgba(0, 0, 0, .04); 398 | border-radius: 2px; 399 | border-top: 0; 400 | background-color: white; 401 | display: none; 402 | z-index: 2; 403 | } 404 | 405 | #suggestions li { 406 | list-style-type: none; 407 | margin: 0; 408 | padding: 5px; 409 | background-color: white; 410 | border-bottom: 1px solid #D8D8D8; 411 | } 412 | 413 | #suggestions .match .tag-name { 414 | color: #3cb568; 415 | font-weight: 600; 416 | } 417 | 418 | #suggestions li:hover, #suggestions .selected { 419 | background-color: #F5F5F5; 420 | } 421 | 422 | #suggestions li:last-of-type { 423 | border-bottom: 0; 424 | } 425 | 426 | .tag-name { 427 | margin-right: 5px; 428 | } 429 | 430 | /* -------------------------------------------- */ 431 | 432 | .query, .query a { 433 | color: white; 434 | background-color: #36b2e8; 435 | } 436 | 437 | #search_form { 438 | display: none; 439 | } 440 | 441 | .indextagbg { 442 | background-color: rgba(0, 0, 0, 0.15); 443 | width: 90%; 444 | } 445 | 446 | .tilde { 447 | color: #3cb568; 448 | font-size: 30px; 449 | margin-top: -13px; 450 | margin-bottom: 10px; 451 | } 452 | 453 | .sort li { 454 | display: inline-block; 455 | margin-right: 10px; 456 | } 457 | 458 | #filter { 459 | position: fixed; 460 | bottom: 0; 461 | } 462 | 463 | /* -------------------------------------------- */ 464 | 465 | .description-container { 466 | position: relative; 467 | display: none; 468 | } 469 | 470 | .triangle { 471 | position: absolute; 472 | width: 8px; 473 | height: 8px; 474 | transform: rotate(45deg); 475 | background-color: #F5F5F5; 476 | top: -4px; 477 | left: 7px; 478 | border-left: 1px solid rgba(0, 0, 0, 0.15); 479 | border-top: 1px solid rgba(0, 0, 0, 0.15); 480 | z-index: 2; 481 | } 482 | 483 | .description { 484 | border: 1px solid rgba(0, 0, 0, 0.15); 485 | position: relative; 486 | color: rgb(75, 75, 75); 487 | background-color: #F5F5F5; 488 | max-width: 535px; 489 | padding: 7px; 490 | margin: 10px 0 10px 0; 491 | overflow: auto; 492 | white-space: normal; 493 | font-size: 12px; 494 | border-radius: 2px; 495 | box-sizing: border-box; 496 | } 497 | 498 | .description h1, 499 | .description h2, 500 | .description h3, 501 | .description h4, 502 | .description h5, 503 | .description h6 { 504 | font-weight: 300; 505 | margin-top: 5px; 506 | margin-bottom: 5px; 507 | } 508 | 509 | .description h2 { 510 | margin-top: 20px; 511 | } 512 | 513 | .description h3 { 514 | margin-top: 15px; 515 | } 516 | 517 | .description li { 518 | color: inherit; 519 | font-size: 12px; 520 | } 521 | 522 | .description ul, ol { 523 | margin-left: 25px; 524 | } 525 | 526 | .description ol li { 527 | list-style-type: decimal; 528 | } 529 | 530 | .description ul li { 531 | list-style-type: disc; 532 | } 533 | 534 | .description > *:first-child { 535 | margin-top: 0; 536 | } 537 | 538 | .description > *:last-child { 539 | margin-bottom: 0; 540 | } 541 | 542 | .description blockquote { 543 | color: gray; 544 | margin: 2px 0 2px 0; 545 | padding: 1px 0 1px 10px; 546 | border-left: 3px solid rgba(0, 0, 0, .3); 547 | } 548 | 549 | .description p { 550 | line-height: 1.1rem; 551 | margin: 5px 0 5px 0; 552 | } 553 | 554 | .description a { 555 | color: #3F91CC; 556 | font-size: 12px; 557 | text-decoration: underline; 558 | } 559 | 560 | @media not all and (max-width: 565px) { 561 | .extra-nav .nav-item:not(:last-child)::after { 562 | content: "•"; 563 | color: #3F91CC; 564 | padding: 0 5px; 565 | } 566 | } 567 | 568 | @media all and (max-width: 565px) { 569 | .extra-nav .nav-item:not(:last-child)::after { 570 | } 571 | 572 | .extra-nav .nav-item { 573 | display: block; 574 | text-align: center; 575 | } 576 | 577 | .extra-nav .nav-item:not(:last-child) { 578 | margin-bottom: 5px; 579 | } 580 | 581 | } 582 | -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/js/app.js: -------------------------------------------------------------------------------- 1 | $.fn.focusTextToEnd = function() { 2 | this.focus(); 3 | let $thisVal = this.val(); 4 | this.val('').val($thisVal); 5 | return this; 6 | }; 7 | 8 | const root_url = "/"; 9 | let params = new URLSearchParams(location.search); 10 | let all_tags = ""; 11 | let selectedIdx = -1; 12 | let lastPrefix = ""; 13 | 14 | function splitTags(string) { 15 | return string.trim().split(/[\s,]+/); 16 | } 17 | 18 | function changeParams(func) { 19 | func(); 20 | window.history.replaceState({}, '', `${location.pathname}?${params}`); 21 | } 22 | 23 | function getAllTags() { 24 | $.ajax({ 25 | url: root_url + 'api/tags/', 26 | dataType: 'json', 27 | success: function(data) { 28 | all_tags = data["tags"]; 29 | } 30 | }); 31 | } 32 | 33 | function loadMark(id) { 34 | $.ajax({ 35 | url: root_url + 'api/mark/' + id, 36 | data: { 37 | 'id': id 38 | }, 39 | dataType: 'json', 40 | success: function(data) { 41 | console.log(data); 42 | } 43 | }); 44 | } 45 | 46 | function editMarkForm(id, form) { 47 | $.ajax({ 48 | url: root_url + 'block/mark/' + id, 49 | dataType: 'html', 50 | success: function(data) { 51 | appendForm(data, form); 52 | $("#id_name").focusTextToEnd(); 53 | $("#id_description").prop("style").height = $("#id_description").prop("scrollHeight") + "px"; 54 | 55 | } 56 | }); 57 | } 58 | 59 | function getTitle(url) { 60 | $.ajax({ 61 | type: "POST", 62 | url: root_url + 'api/get_title/', 63 | data: {'url': url}, 64 | dataType: 'json', 65 | success: function(data) { 66 | if (!("error" in data)) { 67 | if ($("#id_name").val() == "") { 68 | $("#id_name").val(data["url"]); 69 | } 70 | } 71 | } 72 | }); 73 | } 74 | 75 | function editMark(id) { 76 | $.post({ 77 | url: root_url + 'block/mark', 78 | data: $("#editform" + id).serialize(), 79 | success: function(data) { 80 | console.log(data); 81 | } 82 | }); 83 | } 84 | 85 | function deleteMark(id) { 86 | $.post({ 87 | url: root_url + 'api/delete_mark/' + id + '/', 88 | success: function(data) { 89 | console.log(data); 90 | location.reload(); 91 | } 92 | }); 93 | } 94 | 95 | function deleteMarks() { 96 | $.post({ 97 | url: root_url + 'api/delete_marks/', 98 | data: $("#edit_multiple_form").serialize(), 99 | success: function(data) { 100 | location.reload(); 101 | } 102 | }); 103 | } 104 | 105 | function bumpMark(id) { 106 | $.post({ 107 | url: root_url + 'api/bump_mark/' + id + '/', 108 | success: function(data) { 109 | console.log(data); 110 | location.reload(); 111 | } 112 | }); 113 | } 114 | 115 | function wayback(mark_url, mark_date, el) { 116 | $.ajax({ 117 | url: `https://archive.org/wayback/available?url=${mark_url}×tamp=${mark_date}&callback=?`, 118 | jsonp: "callback", 119 | dataType: "jsonp", 120 | success: function(data) { 121 | console.log(data); 122 | if ("archived_snapshots" in data && 123 | "closest" in data["archived_snapshots"] && 124 | "url" in data["archived_snapshots"]["closest"]) { 125 | window.location.href = data["archived_snapshots"]["closest"]["url"]; 126 | } else { 127 | el.classList.add("failed-action"); 128 | el.classList.remove("wayback_btn"); 129 | el.removeAttribute("href"); 130 | } 131 | } 132 | }); 133 | } 134 | 135 | function appendForm(form, el) { 136 | el.parent().parent().append(form); 137 | } 138 | 139 | function filterSuggestions(prefix, field) { 140 | let list = $("#suggestions"); 141 | if (prefix) { 142 | list.empty(); 143 | 144 | for (i = 0; i < all_tags.length; ++i) { 145 | if (all_tags[i]["name"].startsWith(prefix)) { 146 | numberElement = $("").text(all_tags[i]["num_marks"]); 147 | numberElement.addClass("number"); 148 | 149 | tagName = $("").text(all_tags[i]["name"]); 150 | tagName.addClass("tag-name"); 151 | 152 | let listElement = $("
  • ").append(tagName); 153 | listElement.append(numberElement); 154 | 155 | if (prefix == all_tags[i]["name"]) { 156 | listElement.addClass("match"); 157 | } 158 | 159 | list.append(listElement); 160 | } 161 | } 162 | 163 | if (list.children().length > 0) { 164 | offset = field.offset(); 165 | input_height = field.outerHeight(); 166 | console.log(field); 167 | $("#suggestions").css({'top' : (offset.top + input_height) + 'px', 168 | 'left' : (offset.left) + 'px', 169 | 'display': 'block', 170 | 'width': field.innerWidth() + 'px'}); 171 | } else { 172 | $("#suggestions").css({'display': 'none'}); 173 | } 174 | } else { 175 | $("#suggestions").css({'display': 'none'}); 176 | } 177 | } 178 | 179 | function completeWithSuggestedTag(selectedTag, field) { 180 | let tags = splitTags(field.val()); 181 | let last = tags[tags.length - 1].replace(/ /g,''); 182 | if (last[0] == "-") { last = last.substr(1); } 183 | 184 | if (last != selectedTag) { 185 | field.val(field.val() + selectedTag.slice(last.length - selectedTag.length)); 186 | } 187 | 188 | $("#suggestions").css({'display': 'none'}); 189 | selectedIdx = -1; 190 | } 191 | 192 | $(function() { 193 | populateWithSearchParams(); 194 | getAllTags(); 195 | 196 | if (params.get("expand") == "all") { 197 | $(".description-container").show(); 198 | } 199 | 200 | let mark_id = 0; 201 | 202 | $("#suggestions").on("click", "li", function() { 203 | let fullTag = $(this).find("a").text(); 204 | completeWithSuggestedTag(fullTag, $(document.activeElement)); 205 | }); 206 | 207 | $(".edit_btn").click(function(e) { 208 | e.preventDefault(); 209 | if ($("#edit_mark_form")) { 210 | $("#edit_mark_form").remove(); 211 | } 212 | 213 | mark_id = $(this).attr("mark_id"); 214 | editMarkForm(mark_id, $(this)); 215 | }); 216 | 217 | $(".delete_btn").click(function(e) { 218 | e.preventDefault(); 219 | if (confirm('Are you sure?')) { 220 | mark_id = $(this).attr("mark_id"); 221 | deleteMark(mark_id); 222 | } 223 | }); 224 | 225 | $(".bump_btn").click(function(e) { 226 | e.preventDefault(); 227 | mark_id = $(this).attr("mark_id"); 228 | bumpMark(mark_id); 229 | }); 230 | 231 | $(".wayback_btn").click(function(e) { 232 | e.preventDefault(); 233 | mark_url = $(this).attr("mark_url"); 234 | mark_date = $(this).attr("mark_date"); 235 | wayback(mark_url, mark_date, this); 236 | }); 237 | 238 | $(".info_btn").click(function(e) { 239 | e.preventDefault(); 240 | mark_id = $(this).attr("mark_id"); 241 | $(".description-container[mark_id='" + mark_id + "']").toggle(); 242 | }); 243 | 244 | $(".expand_all_btn").click(function(e) { 245 | e.preventDefault(); 246 | $(".description-container").show(); 247 | params.set("expand", "all"); 248 | window.history.replaceState({}, '', `${location.pathname}?${params}`); 249 | changeParams(function() { 250 | params.set("expand", "all"); 251 | }); // maybe a bit too clever 252 | }); 253 | 254 | $(".collapse_all_btn").click(function(e) { 255 | e.preventDefault(); 256 | $(".description-container").hide(); 257 | changeParams(function() { 258 | params.delete("expand"); 259 | }); 260 | }); 261 | 262 | $(".edit_multiple_btn").click(function(e) { 263 | e.preventDefault(); 264 | $(".edit_checkbox").css({'display': 'inline'}); 265 | $(".edit_multiple_form").css({'display': 'block'}); 266 | $(".selected_actions").css({'display': 'block'}); 267 | $("#id_add_tags").focus(); 268 | }); 269 | 270 | $(".remove_selected_btn").click(function(e) { 271 | e.preventDefault(); 272 | deleteMarks(); 273 | }); 274 | 275 | $(".select_all_btn").click(function(e) { 276 | e.preventDefault(); 277 | $(".edit_checkbox").prop('checked', true); 278 | }); 279 | 280 | $(".deselect_all_btn").click(function(e) { 281 | e.preventDefault(); 282 | $(".edit_checkbox").prop('checked', false); 283 | }); 284 | 285 | $(document).on("submit", "#edit_multiple_form", (function(e) { 286 | e.preventDefault(); 287 | $.post({ 288 | url: root_url + 'api/edit_multiple/', 289 | data: $(this).serialize(), 290 | success: function(data) { 291 | location.reload(); 292 | } 293 | }); 294 | })); 295 | 296 | $(document).on("submit", "#edit_mark_form", (function(e) { 297 | e.preventDefault(); 298 | $.post({ 299 | url: root_url + 'block/mark/' + mark_id + '/', 300 | data: $(this).serialize(), 301 | success: function(data) { 302 | location.reload(); 303 | } 304 | }); 305 | })); 306 | 307 | $("#select_all").click(function(e) { 308 | e.preventDefault(); 309 | $(':checkbox').each(function() { 310 | this.checked = true; 311 | }); 312 | }); 313 | 314 | $("#deselect_all").click(function(e) { 315 | e.preventDefault(); 316 | $(':checkbox').each(function() { 317 | this.checked = false; 318 | }); 319 | }); 320 | 321 | $("#tags_selected_form").submit(function(e) { 322 | e.preventDefault(); 323 | 324 | $.post({ 325 | url: root_url + 'edit_selection/', 326 | data: $(this).serialize(), 327 | success: function(data) { 328 | location.reload(); 329 | } 330 | }); 331 | }); 332 | 333 | $("#show_search_btn").click(function() { 334 | $("#search_form").show(); 335 | $("#id_search_url").focus(); 336 | }); 337 | 338 | $(document).on("change", "#id_url", function(e) { 339 | if ($("#id_name").val() == "") { 340 | getTitle($("#id_url").val()); 341 | } 342 | }); 343 | 344 | // autocomplete ------------------ 345 | 346 | $(document).on("keyup click focus", ".tag_field", function(e) { 347 | let tagsStr = $(this).val(); 348 | 349 | // autocomplete should display when: 350 | // caret is on the last character 351 | // user is not selecting text 352 | if (tagsStr.substr(-1) != " " && 353 | tagsStr.length == this.selectionStart && 354 | this.selectionStart == this.selectionEnd) { 355 | let tags = splitTags(tagsStr); 356 | let last = tags[tags.length - 1].replace(/ /g, ''); 357 | if (last[0] == "-") { last = last.substr(1); } 358 | if (last != lastPrefix) { 359 | filterSuggestions(last, $(this)); 360 | selectedIdx = -1; 361 | lastPrefix = last; 362 | } 363 | } else { 364 | $("#suggestions").css({'display': 'none'}); 365 | } 366 | }); 367 | 368 | function populateWithSearchParams() { 369 | if ($("#id_name").val() == "" && $("#id_url").val() == "") { 370 | let params = (new URL(document.location)).searchParams; 371 | 372 | if (params.get("name")) { 373 | $("#id_name").val(decodeURIComponent(params.get("name"))); 374 | } 375 | 376 | if (params.get("url")) { 377 | $("#id_url").val(decodeURIComponent(params.get("url"))); 378 | } 379 | 380 | if (params.get("tags")) { 381 | $("#id_tags").val(decodeURIComponent(params.get("tags"))); 382 | } 383 | 384 | if (params.get("description")) { 385 | $("#id_description").val(decodeURIComponent(params.get("description"))); 386 | $("#id_description").prop("style").height = $("#id_description").prop("scrollHeight") + "px"; 387 | } 388 | } 389 | } 390 | 391 | $(document).on("keydown", ".tag_field", function(e) { 392 | keyMap = { 393 | "up": 40, 394 | "down": 38, 395 | "esc": 27, 396 | "enter": 13 397 | }; 398 | 399 | switch (e.keyCode) { 400 | case keyMap.up: 401 | suggestionsSelect(1); 402 | e.preventDefault(); 403 | break; 404 | case keyMap.down: 405 | suggestionsSelect(-1); 406 | e.preventDefault(); 407 | break; 408 | case keyMap.esc: 409 | e.preventDefault(); 410 | $("#suggestions").css({'display': 'none'}); 411 | break; 412 | case keyMap.enter: 413 | if ($("#suggestions").css("display") != "none") { 414 | e.preventDefault(); 415 | 416 | const selectedTag = $("#suggestions") 417 | .children() 418 | .eq(selectedIdx) 419 | .find("a") 420 | .text(); 421 | 422 | completeWithSuggestedTag(selectedTag, $(document.activeElement)); 423 | $("#suggestions").css({'display': 'none'}); 424 | } 425 | break; 426 | default: 427 | break; 428 | } 429 | }); 430 | 431 | function suggestionsSelect(direction) { 432 | let list = $("#suggestions"); 433 | list.children().eq(selectedIdx).removeClass("selected"); 434 | selectedIdx += direction; 435 | list.children().eq(selectedIdx).addClass("selected"); 436 | } 437 | 438 | $(document).on("mousedown", "#suggestions", function(e) { 439 | e.preventDefault(); // prevents input from blurring when clicking suggestions 440 | }); 441 | 442 | $(document).on("focusout", ".tag_field", function(e) { 443 | $("#suggestions").css({'display': 'none'}); 444 | }); 445 | }); 446 | -------------------------------------------------------------------------------- /app/marksapp/views.py: -------------------------------------------------------------------------------- 1 | from django.http import ( 2 | HttpResponse, 3 | HttpResponseRedirect, 4 | JsonResponse, 5 | HttpResponseNotFound, 6 | Http404, 7 | ) 8 | from django.shortcuts import render, get_object_or_404 9 | from django.db.models import Count, When, Case, Sum, F, Q 10 | from django.db.models.functions import Lower 11 | from django.contrib.auth.decorators import login_required 12 | from django.contrib.auth.models import User 13 | from django.contrib.auth import authenticate, login 14 | from django.urls import reverse 15 | from django.core import serializers 16 | from django.core.exceptions import ValidationError, ObjectDoesNotExist 17 | from django.contrib.auth import update_session_auth_hash 18 | from django.contrib.auth.forms import PasswordChangeForm 19 | from django.utils.timezone import now 20 | from django.utils.translation import ugettext as _ 21 | from marksapp.models import Bookmark, Tag, Profile 22 | from collections import OrderedDict 23 | from marksapp.misc import tag_regex 24 | import json 25 | import urllib.request 26 | import html 27 | import marksapp.forms as forms 28 | import marksapp.netscape as netscape 29 | import re 30 | import markdown 31 | import csv 32 | 33 | 34 | def paginate(marks, after=None, before=None, limit=100, sort_column="-date_added"): 35 | after_link = before_link = None 36 | 37 | ordering = [sort_column, "-id"] 38 | 39 | marks = marks.order_by(*ordering) 40 | earliest = marks.earliest(*ordering) 41 | latest = marks.latest(*ordering) 42 | 43 | rel_ops = ["gt", "lt"] 44 | 45 | if sort_column.startswith("-"): 46 | normalized_sort_column = sort_column[1:] 47 | rel_ops.reverse() 48 | else: 49 | normalized_sort_column = sort_column 50 | 51 | def get_filter_kwargs(rel_op, entity): 52 | return { 53 | f"{normalized_sort_column}__{rel_op}": getattr( 54 | entity, normalized_sort_column 55 | ) 56 | } 57 | 58 | if after: 59 | kwargs_after = get_filter_kwargs(rel_ops[0], after) 60 | kwargs_equal = get_filter_kwargs("exact", after) 61 | marks = marks.filter( 62 | Q(**kwargs_after) | (Q(**kwargs_equal) & Q(id__lt=after.id)) 63 | ) 64 | 65 | if before: 66 | kwargs_before = get_filter_kwargs(rel_ops[1], before) 67 | kwargs_equal = get_filter_kwargs("exact", before) 68 | marks = marks.filter( 69 | Q(**kwargs_before) | (Q(**kwargs_equal) & Q(id__gt=before.id)) 70 | ) 71 | marks = marks.reverse() 72 | 73 | paginated_marks = list(marks.all()[:limit]) 74 | 75 | if before: 76 | paginated_marks = paginated_marks[::-1] 77 | 78 | if paginated_marks[-1] != latest: 79 | after_link = paginated_marks[-1] 80 | 81 | if paginated_marks[0] != earliest: 82 | before_link = paginated_marks[0] 83 | 84 | return { 85 | "marks": paginated_marks, 86 | "after_link": after_link, 87 | "before_link": before_link, 88 | } 89 | 90 | 91 | def tags_strip_split(tags): 92 | return tags.replace(",", " ").split() if tags else [] 93 | 94 | 95 | def tag_untagged(user): 96 | for mark in Bookmark.objects.filter(user=user, tags__isnull=True): 97 | mark.tags.add(Tag.objects.get_or_create(name="untagged")[0]) 98 | 99 | 100 | def index(request): 101 | if request.user.is_authenticated: 102 | return HttpResponseRedirect(reverse("user_index", args=[request.user.username])) 103 | else: 104 | return HttpResponseRedirect(reverse("guide")) 105 | 106 | 107 | def user_index(request, username): 108 | if not User.objects.filter(username=username).exists(): 109 | raise Http404("User doesn't exist") 110 | 111 | try: 112 | profile = Profile.objects.get(user__username=username) 113 | except ObjectDoesNotExist: 114 | profile = None 115 | 116 | sort = request.GET.get("sort", "") or "name" 117 | 118 | bookmarks = Bookmark.objects.filter(user__username=username) 119 | 120 | if not username == request.user.username: 121 | if profile and profile.visibility == Profile.PRIVATE: 122 | bookmarks = bookmarks.filter(tags__name="public") 123 | else: 124 | bookmarks = bookmarks.exclude(tags__name="private") 125 | 126 | bookmarks = bookmarks.exclude(tags__name__startswith=".") 127 | 128 | top_tags = Tag.objects.filter(bookmark__in=bookmarks).annotate( 129 | num_marks=Count("bookmark") 130 | ) 131 | 132 | if not request.user.is_authenticated: 133 | top_tags = top_tags.exclude(name="private") 134 | 135 | if sort == "quantity": 136 | top_tags = top_tags.order_by("-num_marks", "name") 137 | else: 138 | top_tags = top_tags.order_by("name") 139 | 140 | context = { 141 | "all": bookmarks.count(), 142 | "sort": sort, 143 | "tags": top_tags, 144 | "username": username, 145 | "page_title": "home", 146 | } 147 | return render(request, "index.html", context) 148 | 149 | 150 | def user_profile(request): 151 | username = request.user.username 152 | user_object = User.objects.get(username__iexact=username) 153 | profile_object = Profile.objects.get_or_create(user=request.user)[0] 154 | 155 | if request.method == "POST": 156 | user_form = forms.UserForm(request.POST, instance=user_object) 157 | password_form = PasswordChangeForm(user_object, request.POST) 158 | profile_form = forms.ProfileForm(request.POST, instance=profile_object) 159 | if profile_form.is_valid() and user_form.is_valid(): 160 | user_form.save() 161 | profile_form.save() 162 | return HttpResponseRedirect(reverse("index")) 163 | 164 | if password_form.is_valid(): 165 | password_form.save() 166 | update_session_auth_hash(request, user_object) 167 | return HttpResponseRedirect(reverse("index")) 168 | else: 169 | user_form = forms.UserForm(instance=user_object) 170 | password_form = PasswordChangeForm(user_object) 171 | profile_form = forms.ProfileForm(instance=profile_object) 172 | 173 | context = { 174 | "username": username, 175 | "user_form": user_form, 176 | "profile_form": profile_form, 177 | "password_form": password_form, 178 | "page_title": "profile", 179 | } 180 | 181 | return render(request, "profile.html", context) 182 | 183 | 184 | def user_tag(request, username, slug=None): 185 | tags = slug.split("+") if slug else [] 186 | 187 | return marks(request, username, tags) 188 | 189 | 190 | def get_param(request, param, params, default=None): 191 | value = request.GET.get(param, "") 192 | if value: 193 | params.update({param: value}) 194 | 195 | return value 196 | 197 | 198 | @login_required 199 | def csv_export(request): 200 | response = HttpResponse(content_type="text/csv") 201 | response["Content-Disposition"] = 'attachment; filename="bookmarks.csv"' 202 | 203 | bookmarks = Bookmark.objects.all().filter(user=request.user) 204 | csv_writer = csv.writer(response) 205 | csv_writer.writerow(["url", "title", "tags", "date_added"]) 206 | 207 | for bookmark in bookmarks: 208 | csv_writer.writerow( 209 | [bookmark.url, bookmark.name, bookmark.tags_str(), bookmark.date_added] 210 | ) 211 | 212 | return response 213 | 214 | 215 | def marks(request, username, tags=[]): 216 | get_object_or_404(User, username=username) 217 | 218 | context = {"username": username} 219 | params = {} 220 | 221 | bookmarks = ( 222 | Bookmark.objects.all().prefetch_related("tags").filter(user__username=username) 223 | ) 224 | 225 | limit = 100 226 | 227 | if get_param(request, "search_url", params): 228 | bookmarks = bookmarks.filter(url__contains=params["search_url"]) 229 | 230 | if get_param(request, "search_title", params): 231 | bookmarks = bookmarks.filter(name__search=params["search_title"]) 232 | 233 | if get_param(request, "search_tags", params): 234 | tags.extend(tags_strip_split(params["search_tags"])) 235 | 236 | if get_param(request, "search_description", params): 237 | bookmarks = bookmarks.filter(description__search=params["search_description"]) 238 | 239 | for tag in tags: 240 | bookmarks = bookmarks.filter(tags__name=tag) 241 | 242 | try: 243 | profile = Profile.objects.get(user__username=username) 244 | except ObjectDoesNotExist: 245 | profile = None 246 | 247 | if not username == request.user.username: 248 | if profile and profile.visibility == Profile.PRIVATE: 249 | bookmarks = bookmarks.filter(tags__name="public") 250 | else: 251 | bookmarks = bookmarks.exclude(tags__name="private") 252 | 253 | if not tags: 254 | bookmarks = bookmarks.exclude(tags__name__startswith=".") 255 | 256 | if get_param(request, "sort", params) and params["sort"] == "name": 257 | bookmarks = bookmarks.annotate(name_lower=Lower("name")) 258 | sort_column = "name" 259 | sort = "name" 260 | else: 261 | sort_column = "-date_added" 262 | sort = "date" 263 | 264 | after_mark = None 265 | before_mark = None 266 | 267 | if get_param(request, "after", params): 268 | after_id = int(params["after"]) 269 | after_mark = Bookmark.objects.get(id=after_id) 270 | elif get_param(request, "before", params): 271 | before_id = int(params["before"]) 272 | before_mark = Bookmark.objects.get(id=before_id) 273 | 274 | if bookmarks.exists(): 275 | pagination = paginate(bookmarks, after_mark, before_mark, 100, sort_column) 276 | bookmarks = pagination["marks"] 277 | after_mark = pagination["after_link"] 278 | before_mark = pagination["before_link"] 279 | 280 | params_str = "&".join( 281 | ["{}={}".format(param, value) for param, value in params.items()] 282 | ) 283 | 284 | tag_count = ( 285 | Tag.objects.filter(bookmark__in=bookmarks) 286 | .annotate(num_marks=Count("bookmark")) 287 | .order_by("-num_marks", "name") 288 | ) 289 | 290 | l = locals() 291 | context.update( 292 | { 293 | v: l[v] 294 | for v in [ 295 | "bookmarks", 296 | "sort", 297 | "tag_count", 298 | "tags", 299 | "before_mark", 300 | "after_mark", 301 | "params", 302 | "params_str", 303 | ] 304 | } 305 | ) 306 | 307 | return render(request, "marks.html", context) 308 | 309 | 310 | @login_required 311 | def add_mark(request): 312 | if request.method == "POST": 313 | form = forms.BookmarkForm(request.POST) 314 | # we don't want to expose user to the form but need to validate unique_together! 315 | if form.is_valid(): 316 | if ( 317 | form.cleaned_data["url"] 318 | and Bookmark.objects.filter( 319 | url=form.cleaned_data["url"], user=request.user 320 | ).exists() 321 | ): 322 | existing_mark = Bookmark.objects.get( 323 | url=form.cleaned_data["url"], user=request.user 324 | ) 325 | # TODO: maybe a warning would be good 326 | return HttpResponseRedirect( 327 | reverse( 328 | "mark_permalink", 329 | kwargs={ 330 | "username": request.user.username, 331 | "id": existing_mark.id, 332 | }, 333 | ) 334 | ) 335 | else: 336 | mark = form.save(commit=False) 337 | mark.user = request.user 338 | mark.save() 339 | return HttpResponseRedirect(reverse("index")) 340 | else: 341 | form = forms.BookmarkForm() 342 | 343 | return render(request, "add.html", {"form": form, "page_title": "add"}) 344 | 345 | 346 | def mark_permalink(request, username, id): 347 | user_object = User.objects.get(username__iexact=username) 348 | mark = Bookmark.objects.get(id=id) 349 | 350 | try: 351 | profile = Profile.objects.get(user=user_object) 352 | except ObjectDoesNotExist: 353 | profile = None 354 | 355 | if username == request.user.username: 356 | private = False 357 | else: 358 | if profile and profile.visibility == Profile.PRIVATE: 359 | private = not bool(mark.tags.filter(name__iexact="public").first()) 360 | else: 361 | private = bool(mark.tags.filter(name__iexact="private").first()) 362 | 363 | if user_object == mark.user and not private: 364 | context = {"mark": mark} 365 | 366 | return render(request, "mark_permalink.html", context) 367 | else: 368 | return HttpResponseRedirect( 369 | reverse("user_index", kwargs={"username": username}) 370 | ) 371 | 372 | 373 | @login_required 374 | def delete_mark(request, id): 375 | get_object_or_404(Bookmark, id=id).delete() 376 | 377 | return HttpResponseRedirect(reverse("index")) 378 | 379 | 380 | @login_required 381 | def delete_tag(request, slug): 382 | bookmarks = Bookmark.objects.filter(user=request.user, tags__name=slug) 383 | 384 | for mark in bookmarks: 385 | mark.tags.remove(Tag.objects.get(name=slug)) 386 | 387 | tag_untagged(request.user) 388 | 389 | return HttpResponseRedirect(reverse("index")) 390 | 391 | 392 | @login_required 393 | def import_netscape(request): 394 | if request.method == "POST": 395 | form = forms.NetscapeForm(request.POST, request.FILES) 396 | if form.is_valid(): 397 | netscape.bookmarks_from_file(request.FILES["file"], request.user) 398 | return HttpResponseRedirect(reverse("index")) 399 | else: 400 | form = forms.NetscapeForm() 401 | return render(request, "import.html", {"form": form, "page_title": "import"}) 402 | 403 | 404 | @login_required 405 | def export_json(request): 406 | if Bookmark.objects.filter(user=request.user).exists(): 407 | bookmarks = Bookmark.objects.filter(user__username=request.user.username) 408 | marks_dict = {"marks": []} 409 | for mark in bookmarks: 410 | mark_dict = {} 411 | mark_dict["name"] = mark.name 412 | mark_dict["url"] = mark.url 413 | mark_dict["date_added"] = str(mark.date_added) 414 | mark_dict["tags"] = [] 415 | for tag in mark.tags.all(): 416 | mark_dict["tags"].append(tag.name) 417 | marks_dict["marks"].append(mark_dict) 418 | 419 | return JsonResponse(marks_dict) 420 | 421 | 422 | @login_required 423 | def import_json(request): 424 | if request.method == "POST": 425 | form = forms.ImportJsonForm(request.POST, request.FILES) 426 | marks = request.FILES["file"].read() 427 | marks = json.loads(marks) 428 | 429 | for mark in marks["marks"]: 430 | b = Bookmark.objects.update_or_create(user=request.user, url=mark["url"])[0] 431 | b.name = mark["name"] 432 | b.date_added = mark["date_added"] 433 | 434 | for tag in mark["tags"]: 435 | t = Tag.objects.get_or_create(name=tag)[0] 436 | b.tags.add(t) 437 | 438 | b.save() 439 | else: 440 | form = forms.ImportJsonForm() 441 | return render(request, "import.html", {"form": form}) 442 | 443 | 444 | @login_required 445 | def edit_mark_form(request, id): 446 | mark = Bookmark.objects.get(id=id) 447 | 448 | if request.method == "POST": 449 | form = forms.BookmarkForm(request.POST, instance=mark) 450 | if form.is_valid(): 451 | form.save() 452 | tag_untagged(request.user) 453 | return HttpResponse("success") 454 | else: 455 | form = forms.BookmarkForm(instance=mark) 456 | 457 | return render(request, "edit_mark_form.html", {"form": form}) 458 | 459 | 460 | def changelog(request): 461 | with open("etc/changelog.markdown") as f: 462 | return HttpResponse(markdown.markdown(f.read())) 463 | 464 | 465 | def register(request): 466 | context = {} 467 | context["page_title"] = "register" 468 | 469 | if request.method == "POST": 470 | form = forms.RegistrationForm(request.POST) 471 | # we don't want to expose user to the form but need to validate unique_together! 472 | if form.is_valid(): 473 | context["form"] = form 474 | username = form.cleaned_data["username"] 475 | password = form.cleaned_data["password"] 476 | email = form.cleaned_data["email"] 477 | visibility = form.cleaned_data["visibility"] 478 | 479 | try: 480 | username_db = User.objects.get(username__iexact=username) 481 | form.add_error(None, "Username already exists") 482 | except User.DoesNotExist: 483 | user = User.objects.create_user( 484 | username=username, password=password, email=email 485 | ) 486 | new_user = authenticate(username=username, password=password) 487 | profile = Profile(user=user, visibility=visibility) 488 | profile.save() 489 | 490 | if new_user is not None: 491 | login(request, new_user) 492 | return HttpResponseRedirect(reverse("index")) 493 | else: 494 | form.add_error(None, "Something wrong happened") 495 | return render(request, "registration/registration_form.html", context) 496 | else: 497 | form = forms.RegistrationForm() 498 | context["form"] = form 499 | 500 | return render(request, "registration/registration_form.html", context) 501 | 502 | 503 | def guide(request): 504 | context = {"page_title": "guide"} 505 | return render(request, "guide.html", context) 506 | 507 | 508 | # API --------------------------------- 509 | 510 | 511 | def api_mark(request, id): 512 | mark = Bookmark.objects.get(id=id) 513 | mark_dict = {} 514 | mark_dict["name"] = mark.name 515 | mark_dict["url"] = mark.url 516 | mark_dict["tags"] = mark.tags_str() 517 | 518 | return JsonResponse(mark_dict) 519 | 520 | 521 | def api_tags(request, prefix=None): 522 | tags = Tag.objects.all() 523 | 524 | if request.user.is_authenticated: 525 | tags = Tag.objects.filter(bookmark__user__username=request.user) 526 | 527 | if prefix: 528 | tags = tags.filter(name__startswith=prefix) 529 | 530 | tags = tags.annotate(num_marks=Count("bookmark")).order_by("-num_marks", "name") 531 | 532 | tags_dict = {} 533 | tag_list = [] 534 | for tag in tags: 535 | tag_dict = {} 536 | tag_dict["id"] = tag.id 537 | tag_dict["name"] = tag.name 538 | tag_dict["num_marks"] = tag.num_marks 539 | tag_list.append(tag_dict) 540 | 541 | tags_dict["tags"] = tag_list 542 | return JsonResponse(tags_dict) 543 | 544 | 545 | def get_title(url): 546 | if not (url.startswith("http://") or url.startswith("https://")): 547 | url = "http://" + url 548 | 549 | hdr = { 550 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11", 551 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 552 | "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3", 553 | "Accept-Encoding": "none", 554 | "Accept-Language": "en-US,en;q=0.8", 555 | "Connection": "keep-alive", 556 | } 557 | 558 | req = urllib.request.Request(url, headers=hdr) 559 | 560 | with urllib.request.urlopen(req) as f: 561 | contents = f.read().decode("utf-8", "backslashreplace") 562 | contents = html.unescape(contents) 563 | pattern = re.compile(r"(.+?)") 564 | response = {"url": re.findall(pattern, contents)[0]} 565 | print(response) 566 | return JsonResponse(response) 567 | 568 | 569 | @login_required 570 | def api_get_title(request): 571 | if request.method == "POST": 572 | try: 573 | get_title(request.POST.get("url")) 574 | except: 575 | return JsonResponse({"error": "couldn't load title"}) 576 | 577 | 578 | @login_required 579 | def api_delete_mark(request, id): 580 | mark = Bookmark.objects.get(id=id) 581 | if mark.user == request.user: 582 | mark.delete() 583 | 584 | if request.method == "POST": 585 | return JsonResponse({"status": "success"}) 586 | 587 | 588 | @login_required 589 | def api_bump_mark(request, id): 590 | mark = get_object_or_404(Bookmark, id=id) 591 | if mark.user == request.user: 592 | mark.last_bumped = now() 593 | mark.save() 594 | 595 | if request.method == "POST": 596 | return JsonResponse({"status": "success"}) 597 | 598 | 599 | @login_required 600 | def api_delete_marks(request): 601 | if request.method == "POST": 602 | for mark_id in request.POST.getlist("check_mark"): 603 | mark = Bookmark.objects.get(id=mark_id) 604 | if mark.user == request.user: 605 | mark.delete() 606 | return JsonResponse({"status": "success"}) 607 | return JsonResponse({"error": "wrong request method"}) 608 | 609 | 610 | @login_required 611 | def api_edit_multiple(request): 612 | add_tags = request.POST.getlist("add_tags") 613 | remove_tags = request.POST.getlist("remove_tags") 614 | 615 | for mark_id in request.POST.getlist("check_mark"): 616 | mark = Bookmark.objects.get(id=mark_id) 617 | if mark.user == request.user: 618 | for tag in tags_strip_split(request.POST.get("add_tags")): 619 | t = Tag.objects.get_or_create(name=tag)[0] 620 | mark.tags.add(t) 621 | 622 | for tag in tags_strip_split(request.POST.get("remove_tags")): 623 | try: 624 | if re.match(tag_regex, tag): 625 | t = mark.tags.get(name=tag) 626 | mark.tags.remove(t) 627 | except Tag.DoesNotExist: 628 | t = None 629 | 630 | tag_untagged(request.user) 631 | return JsonResponse({"status": "success"}) 632 | -------------------------------------------------------------------------------- /app/marksapp/static/marksapp/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.2.0 | (c) JS Foundation and other contributors | jquery.org/license */ 2 | !function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.0",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), 3 | a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d)); 4 | },attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("