├── cards ├── __init__.py ├── urls │ ├── __init__.py │ ├── api.py │ └── gui.py ├── views │ ├── __init__.py │ ├── api.py │ └── gui.py ├── migrations │ ├── __init__.py │ ├── 0004_auto_20170925_1220.py │ ├── 0003_auto_20170924_2113.py │ ├── 0006_auto_20171213_1608.py │ ├── 0009_auto_20180217_1524.py │ ├── 0005_auto_20171213_1532.py │ ├── 0008_auto_20180118_2206.py │ ├── 0011_auto_20180319_1112.py │ ├── 0013_card_postpone_until.py │ ├── 0010_card_last_interaction.py │ ├── 0002_auto_20170924_1950.py │ ├── 0014_auto_20180411_0324.py │ ├── 0012_auto_20180331_1348.py │ ├── 0007_auto_20171215_1715.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ ├── card_controls.py │ └── area_rating.py ├── apps.py ├── static │ └── cards.js ├── templates │ ├── templatetags │ │ ├── area_rating.html │ │ └── card_controls.html │ └── cards │ │ ├── card_create_form.html │ │ ├── card_update_form.html │ │ ├── card_confirm_delete.html │ │ ├── card_list.html │ │ └── card_detail.html ├── admin.py ├── serializers.py ├── models.py ├── fixtures │ └── demo_cards.yaml └── tests.py ├── braindump ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── check_card_placement_consistency.py ├── migrations │ ├── __init__.py │ ├── 0003_auto_20180411_0841.py │ ├── 0004_fix_postpone_until_default.py │ ├── 0005_auto_20181207_0528.py │ ├── 0002_split_card_model.py │ └── 0001_initial.py ├── static │ ├── braindump.css │ └── braindump.js ├── apps.py ├── admin.py ├── urls.py ├── signals.py ├── tasks.py ├── models.py └── templates │ └── braindump │ ├── braindump_session.html │ └── braindump_index.html ├── categories ├── __init__.py ├── urls │ ├── __init__.py │ ├── api.py │ └── gui.py ├── views │ ├── __init__.py │ ├── api.py │ ├── gui.py │ └── share_contract_gui.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20170925_1220.py │ ├── 0011_remove_category_last_interaction.py │ ├── 0007_auto_20180318_2307.py │ ├── 0008_auto_20180331_1348.py │ ├── 0004_auto_20171215_1715.py │ ├── 0012_sharecontract_revoked.py │ ├── 0003_category_mode.py │ ├── 0010_auto_20180411_0945.py │ ├── 0005_category_last_interaction.py │ ├── 0001_initial.py │ ├── 0006_category_user.py │ └── 0009_sharecontract.py ├── apps.py ├── static │ └── categories.js ├── signals.py ├── fixtures │ ├── demo_share_contracts.yaml │ ├── demo_users.yaml │ └── demo_categories.yaml ├── templates │ └── categories │ │ ├── sharecontract_request_form.html │ │ ├── category_create_form.html │ │ ├── category_update_form.html │ │ ├── sharecontract_confirm_accept.html │ │ ├── sharecontract_confirm_delete.html │ │ ├── category_confirm_delete.html │ │ ├── sharecontract_list.html │ │ ├── category_list.html │ │ └── category_detail.html ├── exceptions.py ├── admin.py ├── serializers.py ├── forms.py ├── models.py └── tests.py ├── memodrop ├── __init__.py ├── settings │ ├── __init__.py │ ├── _generate_secret_key.py │ ├── production.py.dist │ ├── development.py │ └── base.py ├── static │ ├── markdown.css │ ├── fonts │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ └── LICENSE │ ├── memodrop.css │ ├── userguisettings.js │ ├── markdown.js │ ├── chart-2.7.0 │ │ └── LICENSE │ ├── simplemde-1.11.2 │ │ ├── LICENSE │ │ └── simplemde.min.css │ ├── login.css │ ├── bootstrap-4.0.0 │ │ └── LICENSE │ ├── popper-1.12.9 │ │ └── LICENSE │ ├── fa-4.7.0 │ │ └── LICENSE │ └── jquery-3.2.1 │ │ └── LICENSE ├── __about__.py ├── wsgi.py ├── templates │ ├── 404.html │ ├── 403.html │ ├── 500.html │ ├── base.html │ └── main_authorized.html └── urls.py ├── authentication ├── __init__.py ├── urls │ ├── __init__.py │ ├── api.py │ └── gui.py ├── views │ ├── __init__.py │ ├── api.py │ └── gui.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── check_userguisettings_consistency.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20181203_1430.py │ ├── 0003_auto_20181203_1431.py │ ├── 0004_create_userguisettings.py │ └── 0001_initial.py ├── apps.py ├── serializers.py ├── signals.py ├── templates │ ├── auth │ │ ├── password-change.html │ │ ├── login.html │ │ └── profile.html │ └── authentication │ │ └── userguisettings_form.html ├── admin.py └── models.py ├── MANIFEST.in ├── docs └── screenshots │ ├── braindump_index.png │ ├── category_detail.png │ ├── braindump_session.png │ └── braindump_session_mobile.png ├── scripts ├── standalone_service.sh └── memodrop-q.service ├── .bumpversion.cfg ├── setup.cfg ├── .gitignore ├── .coveragerc ├── .dockerignore ├── .travis.yml ├── .editorconfig ├── Dockerfile ├── manage.py ├── LICENSE ├── setup.py └── README.md /cards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /braindump/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cards/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cards/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /categories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /memodrop/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cards/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /categories/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /categories/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /braindump/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /braindump/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cards/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /categories/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /braindump/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /memodrop/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .development import * 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include */static * 3 | recursive-include */templates * 4 | -------------------------------------------------------------------------------- /memodrop/static/markdown.css: -------------------------------------------------------------------------------- 1 | .CodeMirror, .CodeMirror-scroll { 2 | min-height: 100px; 3 | } 4 | -------------------------------------------------------------------------------- /braindump/static/braindump.css: -------------------------------------------------------------------------------- 1 | #braindump #hint, 2 | #braindump #answer { 3 | display: none; 4 | } 5 | -------------------------------------------------------------------------------- /cards/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CardsConfig(AppConfig): 5 | name = 'cards' 6 | -------------------------------------------------------------------------------- /docs/screenshots/braindump_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/docs/screenshots/braindump_index.png -------------------------------------------------------------------------------- /docs/screenshots/category_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/docs/screenshots/category_detail.png -------------------------------------------------------------------------------- /docs/screenshots/braindump_session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/docs/screenshots/braindump_session.png -------------------------------------------------------------------------------- /categories/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CategoriesConfig(AppConfig): 5 | name = 'categories' 6 | -------------------------------------------------------------------------------- /docs/screenshots/braindump_session_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/docs/screenshots/braindump_session_mobile.png -------------------------------------------------------------------------------- /memodrop/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/memodrop/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /memodrop/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/memodrop/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /memodrop/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/memodrop/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /scripts/standalone_service.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | python manage.py migrate $@ 4 | python manage.py runserver 0.0.0.0:8000 $@ 5 | -------------------------------------------------------------------------------- /memodrop/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeig/memodrop/HEAD/memodrop/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.2.4 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:memodrop/__about__.py] 7 | 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = 4 | manage.py 5 | setup.py 6 | */migrations/ 7 | memodrop/settings/ 8 | venv/ 9 | -------------------------------------------------------------------------------- /categories/static/categories.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#id_name").focus(); 3 | markdownEditor({element: $("#id_description")[0]}) 4 | }); 5 | -------------------------------------------------------------------------------- /categories/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | share_contract_accepted = Signal(providing_args=['pk']) 4 | share_contract_revoked = Signal(providing_args=['pk']) 5 | -------------------------------------------------------------------------------- /braindump/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BraindumpConfig(AppConfig): 5 | name = 'braindump' 6 | 7 | def ready(self): 8 | import braindump.signals 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .coverage 3 | *.egg-info/ 4 | build/ 5 | db.sqlite3* 6 | dist/ 7 | memodrop/settings/production.py 8 | memodrop/settings/development.key 9 | local_scripts/ 10 | venv/ 11 | __pycache__ 12 | -------------------------------------------------------------------------------- /authentication/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthenticationConfig(AppConfig): 5 | name = 'authentication' 6 | 7 | def ready(self): 8 | import authentication.signals 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = . 3 | branch = True 4 | omit = 5 | manage.py 6 | setup.py 7 | memodrop/settings/production.py.dist 8 | memodrop/wsgi.py 9 | venv/* 10 | 11 | [report] 12 | show_missing = True 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .coverage 3 | *.egg-info/ 4 | build/ 5 | db.sqlite3* 6 | dist/ 7 | docs/ 8 | memodrop/settings/production.py 9 | memodrop/settings/development.key 10 | local_scripts/ 11 | venv/ 12 | __pycache__ 13 | -------------------------------------------------------------------------------- /cards/static/cards.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | markdownEditor({element: $("#id_question"), autofocus: true}); 3 | markdownEditor({element: $("#id_hint")}); 4 | markdownEditor({element: $("#id_answer")}); 5 | }); 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: 4 | - "3.7" 5 | dist: xenial 6 | install: 7 | - python setup.py develop 8 | - pip install -e ".[dev]" 9 | script: 10 | - coverage run manage.py test 11 | after_success: 12 | - coverage report 13 | -------------------------------------------------------------------------------- /cards/urls/api.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from cards.views.api import APICardList, APICardDetail 4 | 5 | 6 | urlpatterns = [ 7 | url(r'^$', APICardList.as_view()), 8 | url(r'^(?P[0-9]+)/$', APICardDetail.as_view()), 9 | ] 10 | -------------------------------------------------------------------------------- /cards/templates/templatetags/area_rating.html: -------------------------------------------------------------------------------- 1 | {% load font_awesome %}
{% for a in areas %}{% fa 'fa-check-square' %} {% endfor %}{% for a in missing_areas %}{% fa 'fa-square' %} {% endfor %}
-------------------------------------------------------------------------------- /categories/fixtures/demo_share_contracts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - model: categories.ShareContract 3 | pk: 1 4 | fields: 5 | user: 2 6 | category: 7 7 | accepted: true 8 | - model: categories.ShareContract 9 | pk: 2 10 | fields: 11 | user: 2 12 | category: 8 13 | -------------------------------------------------------------------------------- /categories/urls/api.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from categories.views.api import APICategoryList, APICategoryDetail 4 | 5 | 6 | urlpatterns = [ 7 | url(r'^$', APICategoryList.as_view()), 8 | url(r'^(?P[0-9]+)/$', APICategoryDetail.as_view()), 9 | ] 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | 7 | [*.py] 8 | indent_style = space 9 | indent_size = 4 10 | max_line_length = 120 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.html] 15 | indent_style = space 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /authentication/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from authentication.models import UserGUISettings 4 | 5 | 6 | class UserGUISettingsSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = UserGUISettings 9 | fields = ('enable_markdown_editor',) 10 | -------------------------------------------------------------------------------- /braindump/static/braindump.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | $("#braindump #show-hint").click(function() { 3 | $("#braindump #hint").show(); 4 | $(this).addClass("disabled"); 5 | }); 6 | 7 | $("#braindump #show-answer").click(function() { 8 | $("#braindump #answer").show(); 9 | $(this).addClass("disabled"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /memodrop/__about__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | '__name__', 3 | '__version__', 4 | '__description__', 5 | '__url__', 6 | '__author__', 7 | ] 8 | 9 | __name__ = 'memodrop' 10 | __version__ = '1.2.4' 11 | __description__ = 'Rapid learning process for people with tight schedules.' 12 | __url__ = 'https://github.com/joeig/memodrop' 13 | __author__ = 'Johannes Eiglsperger' 14 | -------------------------------------------------------------------------------- /scripts/memodrop-q.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=memodrop Q 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=memodrop 8 | Group=memodrop 9 | WorkingDirectory=/opt/memodrop/app/ 10 | ExecStart=/opt/memodrop/venv/bin/python manage.py qcluster --settings memodrop.settings.production 11 | RestartSec=15 12 | Restart=always 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /memodrop/settings/_generate_secret_key.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def generate_secret_key(length=50): 6 | chars = ''.join([ 7 | string.ascii_letters, 8 | string.digits, 9 | string.punctuation 10 | ]).replace('\'', '').replace('"', '').replace('\\', '') 11 | 12 | return ''.join([random.SystemRandom().choice(chars) for _ in range(length)]) 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine as build 2 | COPY . . 3 | RUN apk --no-cache add build-base 4 | RUN pip install -e "." 5 | 6 | FROM python:3.6-alpine 7 | ENV PYTHONUNBUFFERED 1 8 | WORKDIR /usr/src/app 9 | COPY --from=build /usr/local/lib/python3.6/site-packages/ /usr/local/lib/python3.6/site-packages/ 10 | COPY . . 11 | EXPOSE 8000 12 | RUN chmod +x ./scripts/*.sh 13 | CMD scripts/standalone_service.sh 14 | -------------------------------------------------------------------------------- /cards/templatetags/card_controls.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag('templatetags/card_controls.html', takes_context=True) 7 | def card_controls(context, card_placement): 8 | """Display control buttons for managing one single card 9 | """ 10 | return { 11 | 'user': context['user'], 12 | 'card_placement': card_placement, 13 | } 14 | -------------------------------------------------------------------------------- /memodrop/static/memodrop.css: -------------------------------------------------------------------------------- 1 | textarea.form-control { 2 | height: 100px; 3 | } 4 | 5 | textarea.form-control { 6 | font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; 7 | } 8 | 9 | .btn-group { 10 | display: flex; 11 | } 12 | 13 | .area-rating { 14 | display: flex; 15 | } 16 | 17 | .card-title, 18 | .card-text { 19 | hyphens: auto; 20 | } 21 | -------------------------------------------------------------------------------- /authentication/signals.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db.models import signals 3 | from django.dispatch import receiver 4 | 5 | from authentication.models import UserGUISettings 6 | 7 | 8 | @receiver(signals.post_save, sender=User) 9 | def create_user_gui_settings(instance, created, **kwargs): 10 | """Creates a UserGUISettings item on user create 11 | """ 12 | if created: 13 | UserGUISettings.objects.create(user=instance) 14 | -------------------------------------------------------------------------------- /authentication/urls/api.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from authentication.views.api import UserGUISettingsDetails, UserGUISettingsDetailsWithDefaults 4 | 5 | urlpatterns = [ 6 | url(r'^settings/$', 7 | UserGUISettingsDetails.as_view(), 8 | name='api-authentication-settings'), 9 | url(r'^settings/with-defaults/$', 10 | UserGUISettingsDetailsWithDefaults.as_view(), 11 | name='api-authentication-settings-with-defaults'), 12 | ] 13 | -------------------------------------------------------------------------------- /memodrop/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for memodrop 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.11/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", "memodrop.settings.production") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /cards/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cards.models import Card 4 | 5 | 6 | class CardAdmin(admin.ModelAdmin): 7 | list_display = ('question', 'category') 8 | list_filter = ('category',) 9 | search_fields = ('question', 'hint', 'answer') 10 | 11 | def get_readonly_fields(self, request, obj=None): 12 | if obj: 13 | return self.readonly_fields + ('category',) 14 | return self.readonly_fields 15 | 16 | 17 | admin.site.register(Card, CardAdmin) 18 | -------------------------------------------------------------------------------- /memodrop/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% block custom_stylesheet_links %} 4 | {% endblock %} 5 | {% block custom_body_classes %}text-center{% endblock %} 6 | {% block title %}Not Found (404){% endblock %} 7 | {% block body %} 8 |
9 |

Not Found!

10 |

Sorry, but we were unable to serve the desired resource.

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /cards/migrations/0004_auto_20170925_1220.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-25 12:20 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0003_auto_20170924_2113'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='card', 17 | options={'ordering': ['area']}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /categories/migrations/0002_auto_20170925_1220.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-25 12:20 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='category', 17 | options={'ordering': ['name']}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /memodrop/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% block custom_stylesheet_links %} 4 | {% endblock %} 5 | {% block custom_body_classes %}text-center{% endblock %} 6 | {% block title %}Forbidden (403){% endblock %} 7 | {% block body %} 8 |
9 |

Access Denied!

10 |

Sorry, you are not authorized to access the desired resource.

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /authentication/templates/auth/password-change.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %}Change your password{% endblock %} 4 | {% block content %} 5 |

Change your password

6 |
7 | {% csrf_token %} 8 | {% bootstrap_form form %} 9 | {% buttons %} 10 | 11 | {% endbuttons %} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /cards/urls/gui.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from cards.views.gui import CardCreate, CardDelete, CardDetail, CardList, CardUpdate 4 | 5 | urlpatterns = [ 6 | url(r'^$', CardList.as_view(), name='card-list'), 7 | url(r'^add/$', CardCreate.as_view(), name='card-create'), 8 | url(r'^(?P[0-9]+)/$', CardDetail.as_view(), name='card-detail'), 9 | url(r'^(?P[0-9]+)/edit/$', CardUpdate.as_view(), name='card-update'), 10 | url(r'^(?P[0-9]+)/delete/$', CardDelete.as_view(), name='card-delete'), 11 | ] 12 | -------------------------------------------------------------------------------- /categories/templates/categories/sharecontract_request_form.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %}Share category{% endblock %} 4 | {% block content %} 5 |

Share category

6 |
7 | {% csrf_token %} 8 | {% bootstrap_form form %} 9 | {% buttons %} 10 | 11 | {% endbuttons %} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /braindump/migrations/0003_auto_20180411_0841.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-11 13:41 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('braindump', '0002_split_card_model'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='cardplacement', 17 | options={'ordering': ['area']}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /categories/migrations/0011_remove_category_last_interaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-11 22:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0010_auto_20180411_0945'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='category', 17 | name='last_interaction', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /authentication/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from authentication.models import UserGUISettings 4 | 5 | 6 | class UserGUISettingsAdmin(admin.ModelAdmin): 7 | list_display = ('user', 'enable_markdown_editor') 8 | list_filter = ('user', 'enable_markdown_editor') 9 | search_fields = ('user', 'enable_markdown_editor') 10 | readonly_fields = ('user',) 11 | 12 | def has_delete_permission(self, request, obj=None): 13 | return False 14 | 15 | 16 | admin.site.register(UserGUISettings, UserGUISettingsAdmin) 17 | -------------------------------------------------------------------------------- /categories/migrations/0007_auto_20180318_2307.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-18 23:07 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0006_category_user'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='category', 17 | old_name='user', 18 | new_name='owner', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /authentication/migrations/0002_auto_20181203_1430.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.13 on 2018-12-03 20:30 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('authentication', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='userguisettings', 17 | options={'verbose_name_plural': 'user gui settings'}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /memodrop/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% block custom_stylesheet_links %} 4 | {% endblock %} 5 | {% block custom_body_classes %}text-center{% endblock %} 6 | {% block title %}Internal Server Error (500){% endblock %} 7 | {% block body %} 8 |
9 |

Something went wrong!

10 |

Sorry, but we were unable to serve your request. The administrator has been notified.

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /cards/templatetags/area_rating.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag('templatetags/area_rating.html') 7 | def area_rating(area): 8 | """Display a graphic indicator showing the area 9 | """ 10 | areas = list() 11 | missing_areas = list() 12 | 13 | for a in range(0, area - 1): 14 | areas.append(a) 15 | 16 | for a in range(0, 6 - area): 17 | missing_areas.append(a) 18 | 19 | return { 20 | 'areas': areas, 21 | 'missing_areas': missing_areas, 22 | } 23 | -------------------------------------------------------------------------------- /cards/views/api.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from cards.serializers import CardSerializer 5 | from cards.views.gui import CardBelongsOwnerMixin 6 | 7 | 8 | class APICardList(CardBelongsOwnerMixin, generics.ListCreateAPIView): 9 | permission_classes = (IsAuthenticated,) 10 | serializer_class = CardSerializer 11 | 12 | 13 | class APICardDetail(CardBelongsOwnerMixin, generics.RetrieveUpdateDestroyAPIView): 14 | permission_classes = (IsAuthenticated,) 15 | serializer_class = CardSerializer 16 | -------------------------------------------------------------------------------- /categories/migrations/0008_auto_20180331_1348.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-31 13:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0007_auto_20180318_2307'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='category', 17 | name='description', 18 | field=models.TextField(verbose_name='Description'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /categories/migrations/0004_auto_20171215_1715.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2017-12-15 17:15 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0003_category_mode'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='category', 17 | name='description', 18 | field=models.TextField(verbose_name='Description (Markdown)'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /categories/migrations/0012_sharecontract_revoked.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.13 on 2018-09-02 16:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0011_remove_category_last_interaction'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='sharecontract', 17 | name='revoked', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /authentication/migrations/0003_auto_20181203_1431.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.13 on 2018-12-03 20:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('authentication', '0002_auto_20181203_1430'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='userguisettings', 17 | options={'verbose_name': 'User GUI settings', 'verbose_name_plural': 'User GUI settings'}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /categories/migrations/0003_category_mode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-12-13 23:02 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0002_auto_20170925_1220'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='category', 17 | name='mode', 18 | field=models.IntegerField(choices=[(1, 'Strict'), (2, 'Defensive')], default=1), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /categories/views/api.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from categories.serializers import CategorySerializer 5 | from categories.views.gui import CategoryBelongsUserMixin 6 | 7 | 8 | class APICategoryList(CategoryBelongsUserMixin, generics.ListCreateAPIView): 9 | permission_classes = (IsAuthenticated,) 10 | serializer_class = CategorySerializer 11 | 12 | 13 | class APICategoryDetail(CategoryBelongsUserMixin, generics.RetrieveUpdateDestroyAPIView): 14 | permission_classes = (IsAuthenticated,) 15 | serializer_class = CategorySerializer 16 | -------------------------------------------------------------------------------- /cards/migrations/0003_auto_20170924_2113.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-24 21:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0002_auto_20170924_1950'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='card', 17 | name='area', 18 | field=models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6')], default=1), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /categories/exceptions.py: -------------------------------------------------------------------------------- 1 | class WorkflowException(Exception): 2 | pass 3 | 4 | 5 | class ShareContractCannotBeAccepted(WorkflowException): 6 | pass 7 | 8 | 9 | class ShareContractCannotBeDeclined(WorkflowException): 10 | pass 11 | 12 | 13 | class ShareContractCannotBeRevoked(WorkflowException): 14 | pass 15 | 16 | 17 | class ShareContractAlreadyRevoked(WorkflowException): 18 | pass 19 | 20 | 21 | class ShareContractUserIsOwner(WorkflowException): 22 | pass 23 | 24 | 25 | class ShareContractAlreadyExists(WorkflowException): 26 | pass 27 | 28 | 29 | class ShareContractUserDoesNotExist(WorkflowException): 30 | pass 31 | -------------------------------------------------------------------------------- /cards/migrations/0006_auto_20171213_1608.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-12-13 16: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 = [ 11 | ('cards', '0005_auto_20171213_1532'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='card', 17 | name='_area', 18 | field=models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6')], default=1, verbose_name='Area'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from categories.models import Category, ShareContract 4 | 5 | 6 | class CategoryAdmin(admin.ModelAdmin): 7 | list_display = ('name', 'mode', 'owner') 8 | list_filter = ('mode', 'owner') 9 | search_fields = ('name', 'description') 10 | readonly_fields = ('owner',) 11 | 12 | 13 | class ShareContractAdmin(admin.ModelAdmin): 14 | list_display = ('category', 'user', 'accepted', 'revoked') 15 | list_filter = list_display 16 | readonly_fields = list_display 17 | 18 | 19 | admin.site.register(Category, CategoryAdmin) 20 | admin.site.register(ShareContract, ShareContractAdmin) 21 | -------------------------------------------------------------------------------- /categories/migrations/0010_auto_20180411_0945.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-11 14:45 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 | ('categories', '0009_sharecontract'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterUniqueTogether( 18 | name='sharecontract', 19 | unique_together=set([('category', 'user')]), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /braindump/migrations/0004_fix_postpone_until_default.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-15 22:09 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 | dependencies = [ 12 | ('braindump', '0003_auto_20180411_0841'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='cardplacement', 18 | name='postpone_until', 19 | field=models.DateTimeField(blank=True, default=django.utils.timezone.now), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /categories/templates/categories/category_create_form.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | 3 | {% load bootstrap4 %} 4 | {% load static %} 5 | 6 | {% block title %}Category{% endblock %} 7 | {% block custom_javascript_tags %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Create category

13 |
14 | {% csrf_token %} 15 | {% bootstrap_form form %} 16 | {% buttons %} 17 | 18 | {% endbuttons %} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /categories/templates/categories/category_update_form.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | 3 | {% load bootstrap4 %} 4 | {% load static %} 5 | 6 | {% block title %}Category{% endblock %} 7 | {% block custom_javascript_tags %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Update {{ category }}

13 |
14 | {% csrf_token %} 15 | {% bootstrap_form form %} 16 | {% buttons %} 17 | 18 | {% endbuttons %} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /cards/migrations/0009_auto_20180217_1524.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-02-17 15:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0008_auto_20180118_2206'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='card', 17 | options={'ordering': ['area']}, 18 | ), 19 | migrations.RenameField( 20 | model_name='card', 21 | old_name='_area', 22 | new_name='area', 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /authentication/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | 5 | class UserGUISettings(models.Model): 6 | """UserGUISettings 7 | 8 | Extend the user model with particular GUI settings. 9 | 10 | Null = Use system's default 11 | True = Enable feature explicitly 12 | False = Disable feature explicitly 13 | """ 14 | user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True) 15 | enable_markdown_editor = models.NullBooleanField(verbose_name='Enable Markdown editor', null=True) 16 | 17 | class Meta: 18 | verbose_name = "User GUI settings" 19 | verbose_name_plural = verbose_name 20 | -------------------------------------------------------------------------------- /cards/migrations/0005_auto_20171213_1532.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-12-13 15:32 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0004_auto_20170925_1220'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='card', 17 | options={'ordering': ['_area']}, 18 | ), 19 | migrations.RenameField( 20 | model_name='card', 21 | old_name='area', 22 | new_name='_area', 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /cards/migrations/0008_auto_20180118_2206.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2018-01-18 22:06 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('cards', '0007_auto_20171215_1715'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='card', 18 | name='category', 19 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cards', to='categories.Category'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /cards/migrations/0011_auto_20180319_1112.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-19 11:12 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('cards', '0010_card_last_interaction'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='card', 18 | name='category', 19 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cards', to='categories.Category'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /cards/migrations/0013_card_postpone_until.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-07 18:47 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 | dependencies = [ 12 | ('cards', '0012_auto_20180331_1348'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='card', 18 | name='postpone_until', 19 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /cards/migrations/0010_card_last_interaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-11 11:00 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 | dependencies = [ 12 | ('cards', '0009_auto_20180217_1524'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='card', 18 | name='last_interaction', 19 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /braindump/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from braindump.models import CardPlacement 4 | 5 | 6 | class CardPlacementAdmin(admin.ModelAdmin): 7 | list_display = ('card', 'user', 'area', 'postpone_until', 'last_interaction') 8 | list_filter = ('user',) 9 | 10 | def get_readonly_fields(self, request, obj=None): 11 | if obj: 12 | return self.readonly_fields + ('card', 'user') 13 | return self.readonly_fields 14 | 15 | def has_add_permission(self, request): 16 | return False 17 | 18 | def has_delete_permission(self, request, obj=None): 19 | return False 20 | 21 | 22 | admin.site.register(CardPlacement, CardPlacementAdmin) 23 | -------------------------------------------------------------------------------- /categories/migrations/0005_category_last_interaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-11 11:00 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 | dependencies = [ 12 | ('categories', '0004_auto_20171215_1715'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='category', 18 | name='last_interaction', 19 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /cards/migrations/0002_auto_20170924_1950.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-24 19:50 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='card', 17 | name='area', 18 | field=models.IntegerField(default=1), 19 | ), 20 | migrations.AlterField( 21 | model_name='card', 22 | name='hint', 23 | field=models.TextField(blank=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /authentication/templates/authentication/userguisettings_form.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | 3 | {% load bootstrap4 %} 4 | {% load static %} 5 | 6 | {% block title %}Change your global GUI settings{% endblock %} 7 | 8 | {% block content %} 9 |

Change your global GUI settings

10 |
11 | {% csrf_token %} 12 | {% bootstrap_form form layout='horizontal' %} 13 |

"Unknown" applies the system-wide default behavior.

14 | {% buttons %} 15 | 16 | {% endbuttons %} 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /categories/templates/categories/sharecontract_confirm_accept.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %}Accept confirmation{% endblock %} 4 | {% block content %} 5 |

Accept confirmation

6 |
7 | {% csrf_token %} 8 |

{{ share_contract.category.owner }} invited you to access {{ share_contract.category }}.

9 | {% buttons %} 10 | 11 | 12 | {% endbuttons %} 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /categories/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-24 18:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Category', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=128)), 21 | ('description', models.TextField()), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /authentication/migrations/0004_create_userguisettings.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def create_userguisettings(apps, schema_editor): 5 | """Creates UserGuiSettings items for existing users 6 | """ 7 | User = apps.get_model('auth', 'User') 8 | UserGUISettings = apps.get_model('authentication', 'UserGUISettings') 9 | 10 | UserGUISettings.objects.bulk_create( 11 | UserGUISettings(user=user) 12 | for user in User.objects.all() 13 | ) 14 | 15 | 16 | class Migration(migrations.Migration): 17 | dependencies = [ 18 | ('authentication', '0003_auto_20181203_1431'), 19 | ] 20 | 21 | operations = [ 22 | migrations.RunPython(create_userguisettings, migrations.RunPython.noop), 23 | ] 24 | -------------------------------------------------------------------------------- /cards/templates/cards/card_create_form.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | 3 | {% load bootstrap4 %} 4 | {% load static %} 5 | 6 | {% block title %}Card{% endblock %} 7 | {% block custom_javascript_tags %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Create card

13 |
14 | {% csrf_token %} 15 | {% bootstrap_form form %} 16 | {% buttons %} 17 | 18 | 19 | {% endbuttons %} 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /cards/templates/cards/card_update_form.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | 3 | {% load bootstrap4 %} 4 | {% load static %} 5 | 6 | {% block title %}Card{% endblock %} 7 | {% block custom_javascript_tags %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Update {{ card }}

13 |
14 | {% csrf_token %} 15 | {% bootstrap_form form %} 16 | {% buttons %} 17 | 18 | 19 | {% endbuttons %} 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /braindump/migrations/0005_auto_20181207_0528.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.17 on 2018-12-07 11:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('braindump', '0004_fix_postpone_until_default'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddIndex( 16 | model_name='cardplacement', 17 | index=models.Index(fields=['user'], name='braindump_c_user_id_4ce371_idx'), 18 | ), 19 | migrations.AddIndex( 20 | model_name='cardplacement', 21 | index=models.Index(fields=['card'], name='braindump_c_card_id_efea88_idx'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /categories/migrations/0006_category_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-18 22:44 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 | ('categories', '0005_category_last_interaction'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='category', 20 | name='user', 21 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 22 | preserve_default=False, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /categories/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from categories.models import Category 4 | 5 | 6 | class CategorySerializer(serializers.ModelSerializer): 7 | cards = serializers.PrimaryKeyRelatedField(many=True, read_only=True) 8 | 9 | class Meta: 10 | model = Category 11 | fields = ('id', 'name', 'description', 'mode', 'cards') 12 | read_only_fields = ('id',) 13 | 14 | def _get_user(self): 15 | """Try to retrieve the current user 16 | """ 17 | user = None 18 | request = self.context.get('request') 19 | if request and hasattr(request, 'user'): 20 | user = request.user 21 | return user 22 | 23 | def create(self, validated_data): 24 | validated_data['owner'] = self._get_user() 25 | 26 | return super().create(validated_data) 27 | -------------------------------------------------------------------------------- /cards/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from cards.models import Card 4 | from categories.models import Category 5 | 6 | 7 | class CategoryForeignKey(serializers.PrimaryKeyRelatedField): 8 | def _get_user(self): 9 | """Try to retrieve the current user 10 | """ 11 | user = None 12 | request = self.context.get('request') 13 | if request and hasattr(request, 'user'): 14 | user = request.user 15 | return user 16 | 17 | def get_queryset(self): 18 | return Category.owned_objects.all(self._get_user()) 19 | 20 | 21 | class CardSerializer(serializers.ModelSerializer): 22 | category = CategoryForeignKey() 23 | 24 | class Meta: 25 | model = Card 26 | fields = ('id', 'question', 'answer', 'hint', 'category') 27 | read_only_fields = ('id',) 28 | -------------------------------------------------------------------------------- /memodrop/static/userguisettings.js: -------------------------------------------------------------------------------- 1 | var userGUISettingsSessionStorageKey = "userGUISettings"; 2 | 3 | function refreshUserGUISettings(apiURL) { 4 | $.ajax({ 5 | url: apiURL, 6 | dataType: "json" 7 | }).done(function(data) { 8 | console.log("userGUISettings refreshed successfully"); 9 | sessionStorage.setItem(userGUISettingsSessionStorageKey, JSON.stringify(data)); 10 | }).fail(function(jqXHR, textStatus, errorThrown) { 11 | console.log("userGUISettings refresh failed: " + errorThrown); 12 | }); 13 | } 14 | 15 | function getUserGUISettingsAttribute(key) { 16 | var userGUISettings = JSON.parse(sessionStorage.getItem(userGUISettingsSessionStorageKey)); 17 | return userGUISettings[key]; 18 | } 19 | 20 | function flushUserGUISettings() { 21 | sessionStorage.removeItem(userGUISettingsSessionStorageKey); 22 | } 23 | -------------------------------------------------------------------------------- /cards/migrations/0014_auto_20180411_0324.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-11 08:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0013_card_postpone_until'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='card', 17 | options={}, 18 | ), 19 | migrations.RemoveField( 20 | model_name='card', 21 | name='area', 22 | ), 23 | migrations.RemoveField( 24 | model_name='card', 25 | name='last_interaction', 26 | ), 27 | migrations.RemoveField( 28 | model_name='card', 29 | name='postpone_until', 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /categories/fixtures/demo_users.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - model: auth.User 3 | pk: 1 4 | fields: 5 | username: admin 6 | first_name: John 7 | last_name: Doe 8 | email: john.doe@example.tld 9 | # password: superuser 10 | password: pbkdf2_sha256$36000$tgUMsUFanHGd$Y/aJ+UVhFIyNMDlvsioOz0rqqYKhMQWFII6bl7p1/zE= 11 | is_superuser: True 12 | is_staff: True 13 | - model: auth.User 14 | pk: 2 15 | fields: 16 | username: user 17 | first_name: Jane 18 | last_name: Doe 19 | email: jane.doe@example.tld 20 | # password: regularuser 21 | password: pbkdf2_sha256$36000$TgHy6u3qsXEt$aoK+YZkmupz4v1HqgttcLckpPMc9+dEPxAJ3enFlCCs= 22 | - model: auth.User 23 | pk: 3 24 | fields: 25 | username: empty 26 | email: empty@example.tld 27 | # password: emptyuser 28 | password: pbkdf2_sha256$36000$miNqB5snx0m6$ROWnm9rlCEG+pjgFAW0UiZileqolgRlyPn5JCLZ4qdQ= 29 | -------------------------------------------------------------------------------- /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", "memodrop.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 | -------------------------------------------------------------------------------- /cards/templates/templatetags/card_controls.html: -------------------------------------------------------------------------------- 1 |
2 | {% if card_placement %} 3 | Reset 4 | Expedite 5 | {% endif %} 6 | 7 | Update 8 | Delete 9 |
10 | -------------------------------------------------------------------------------- /categories/templates/categories/sharecontract_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %}Revoke access confirmation{% endblock %} 4 | {% block content %} 5 |

Revoke access confirmation

6 |
7 | {% csrf_token %} 8 |

Are you sure you want to revoke the access for {{ share_contract.user }} to the category {{ share_contract.category }}?

11 |

{{ share_contract.user }} will be having a exact copy of this category and all cards.

12 | {% buttons %} 13 | 14 | {% endbuttons %} 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /cards/migrations/0012_auto_20180331_1348.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-31 13:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0011_auto_20180319_1112'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='card', 17 | name='answer', 18 | field=models.TextField(verbose_name='Answer'), 19 | ), 20 | migrations.AlterField( 21 | model_name='card', 22 | name='hint', 23 | field=models.TextField(blank=True, verbose_name='Hint'), 24 | ), 25 | migrations.AlterField( 26 | model_name='card', 27 | name='question', 28 | field=models.TextField(verbose_name='Question'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /braindump/migrations/0002_split_card_model.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def split_card_model(apps, schema_editor): 5 | """Splits attributes of Card into CardPlacement 6 | """ 7 | Card = apps.get_model('cards', 'Card') 8 | CardPlacement = apps.get_model('braindump', 'CardPlacement') 9 | 10 | CardPlacement.objects.bulk_create( 11 | CardPlacement(area=old_object.area, 12 | card=old_object, 13 | user=old_object.category.owner, 14 | last_interaction=old_object.last_interaction, 15 | postpone_until=old_object.postpone_until) 16 | for old_object in Card.objects.all() 17 | ) 18 | 19 | 20 | class Migration(migrations.Migration): 21 | dependencies = [ 22 | ('braindump', '0001_initial'), 23 | ] 24 | 25 | operations = [ 26 | migrations.RunPython(split_card_model, migrations.RunPython.noop), 27 | ] 28 | -------------------------------------------------------------------------------- /cards/migrations/0007_auto_20171215_1715.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2017-12-15 17:15 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cards', '0006_auto_20171213_1608'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='card', 17 | name='answer', 18 | field=models.TextField(verbose_name='Answer (Markdown)'), 19 | ), 20 | migrations.AlterField( 21 | model_name='card', 22 | name='hint', 23 | field=models.TextField(blank=True, verbose_name='Hint (Markdown)'), 24 | ), 25 | migrations.AlterField( 26 | model_name='card', 27 | name='question', 28 | field=models.TextField(verbose_name='Question (Markdown)'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /authentication/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.13 on 2018-12-03 19:18 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 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='UserGUISettings', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('enable_markdown_editor', models.NullBooleanField(verbose_name='Enable Markdown editor')), 24 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /cards/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-24 19:42 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('categories', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Card', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('question', models.TextField()), 23 | ('answer', models.TextField()), 24 | ('hint', models.TextField()), 25 | ('area', models.IntegerField()), 26 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='categories.Category')), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /memodrop/static/markdown.js: -------------------------------------------------------------------------------- 1 | function markdownEditor(parameters) { 2 | if(!getUserGUISettingsAttribute("enable_markdown_editor")) return; 3 | var simplemde = new SimpleMDE({ 4 | element: parameters.element[0], 5 | autofocus: parameters.autofocus, 6 | forceSync: true, 7 | autoDownloadFontAwesome: false, 8 | spellChecker: false, 9 | status: false, 10 | renderingConfig: { 11 | singleLineBreaks: false 12 | }, 13 | toolbar: [ 14 | "bold", 15 | "italic", 16 | "strikethrough", 17 | "|", 18 | "quote", 19 | "unordered-list", 20 | "ordered-list", 21 | "|", 22 | "link", 23 | "image", 24 | "table", 25 | "code", 26 | "|", 27 | "preview" 28 | ] 29 | }); 30 | try { 31 | parameters.element.attr("required", false); 32 | } catch(TypeError) { 33 | console.log("Element does not have a \"required\" attribute"); 34 | } 35 | return simplemde; 36 | } 37 | -------------------------------------------------------------------------------- /memodrop/static/chart-2.7.0/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Chart.js Contributors 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /categories/migrations/0009_sharecontract.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-11 14:05 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 | ('categories', '0008_auto_20180331_1348'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='ShareContract', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('accepted', models.BooleanField(default=False)), 23 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='share_contracts', to='categories.Category')), 24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Johannes Eiglsperger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /memodrop/static/simplemde-1.11.2/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Next Step Webs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /authentication/urls/gui.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib.auth import views as auth_views 3 | from django.urls import reverse_lazy 4 | 5 | from authentication.views.gui import PasswordChangeDoneView, ProfileView, UserGUISettingsUpdate 6 | 7 | urlpatterns = [ 8 | url(r'^login/$', 9 | auth_views.LoginView.as_view(template_name='auth/login.html'), 10 | name='authentication-login'), 11 | url(r'^logout/$', 12 | auth_views.logout_then_login, 13 | name='authentication-logout'), 14 | url(r'^password-change/$', 15 | auth_views.PasswordChangeView.as_view(template_name='auth/password-change.html', 16 | success_url=reverse_lazy('authentication-password-change-done')), 17 | name='authentication-password-change'), 18 | url(r'^password-change/done/$', 19 | PasswordChangeDoneView.as_view(), 20 | name='authentication-password-change-done'), 21 | url(r'^profile/$', 22 | ProfileView.as_view(), 23 | name='authentication-profile'), 24 | url(r'^settings/$', 25 | UserGUISettingsUpdate.as_view(), 26 | name='authentication-user-gui-settings-update'), 27 | ] 28 | -------------------------------------------------------------------------------- /memodrop/static/login.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | display: -ms-flexbox; 8 | display: -webkit-box; 9 | display: flex; 10 | -ms-flex-align: center; 11 | -ms-flex-pack: center; 12 | -webkit-box-align: center; 13 | align-items: center; 14 | -webkit-box-pack: center; 15 | justify-content: center; 16 | padding-top: 40px; 17 | padding-bottom: 40px; 18 | background-color: #f5f5f5; 19 | } 20 | 21 | .form-signin { 22 | width: 100%; 23 | max-width: 330px; 24 | padding: 15px; 25 | margin: 0 auto; 26 | } 27 | 28 | .form-signin .checkbox { 29 | font-weight: 400; 30 | } 31 | 32 | .form-signin .form-control { 33 | position: relative; 34 | box-sizing: border-box; 35 | height: auto; 36 | padding: 10px; 37 | font-size: 16px; 38 | } 39 | 40 | .form-signin .form-control:focus { 41 | z-index: 2; 42 | } 43 | 44 | .form-signin input[type="text"] { 45 | margin-bottom: -1px; 46 | border-bottom-right-radius: 0; 47 | border-bottom-left-radius: 0; 48 | } 49 | 50 | .form-signin input[type="password"] { 51 | margin-bottom: 10px; 52 | border-top-left-radius: 0; 53 | border-top-right-radius: 0; 54 | } 55 | -------------------------------------------------------------------------------- /memodrop/static/bootstrap-4.0.0/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2018 Twitter, Inc. 4 | Copyright (c) 2011-2018 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /memodrop/static/popper-1.12.9/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2016 Federico Zivolo and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /authentication/views/api.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework import generics 3 | from rest_framework.generics import get_object_or_404 4 | from rest_framework.permissions import IsAuthenticated 5 | 6 | from authentication.models import UserGUISettings 7 | from authentication.serializers import UserGUISettingsSerializer 8 | 9 | 10 | class UserGUISettingsDetails(generics.RetrieveAPIView, generics.UpdateAPIView): 11 | permission_classes = (IsAuthenticated,) 12 | serializer_class = UserGUISettingsSerializer 13 | 14 | def get_object(self): 15 | return get_object_or_404(UserGUISettings, user=self.request.user) 16 | 17 | 18 | class UserGUISettingsDetailsWithDefaults(generics.RetrieveAPIView): 19 | permission_classes = (IsAuthenticated,) 20 | serializer_class = UserGUISettingsSerializer 21 | 22 | def get_object(self): 23 | normalized_object = get_object_or_404(UserGUISettings, user=self.request.user) 24 | # Replace None values with system-wide default values: 25 | for key, value in settings.USER_GUI_SETTINGS_DEFAULTS.items(): 26 | if normalized_object.__getattribute__(key) is None: 27 | normalized_object.__setattr__(key, value) 28 | return normalized_object 29 | -------------------------------------------------------------------------------- /memodrop/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap4 %} 2 | {% load font_awesome %} 3 | {% load static %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}memodrop{% endblock %} 12 | 13 | {% bootstrap_css %} 14 | {% fa_css %} 15 | 16 | 17 | 18 | {% block custom_stylesheet_links %}{% endblock %} 19 | 20 | 21 | {% bootstrap_javascript %} 22 | 23 | {% block custom_javascript_initialization_tags %}{% endblock %} 24 | 25 | 26 | {% block custom_javascript_tags %}{% endblock %} 27 | 28 | 29 | 30 | {% block body %}{% endblock %} 31 | 32 | 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from codecs import open 2 | from os import path 3 | 4 | from setuptools import setup, find_packages 5 | 6 | from memodrop.__about__ import * 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | 10 | with open(path.join(here, 'README.md')) as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name=__name__, 15 | version=__version__, 16 | description=__description__, 17 | long_description=long_description, 18 | url=__url__, 19 | author=__author__, 20 | python_requires='>=3.6, <4', 21 | packages=find_packages(exclude=['docs']), 22 | include_package_data=True, 23 | install_requires=[ 24 | 'Django~=2.2', 25 | 'django-bootstrap4==0.0.8', 26 | 'django-fa~=1.0.0', 27 | 'django-jsonview~=1.2', 28 | 'django-markdown-deux~=1.0', 29 | 'django-q~=1.0', 30 | 'django-sendmail-backend~=0.1', 31 | 'django-watchman~=0.17', 32 | 'djangorestframework~=3.9', 33 | 'markdown2~=2.3', 34 | 'numpy~=1.16', 35 | 'pytz==2019.1', 36 | ], 37 | extras_require={ 38 | 'dev': [ 39 | 'bumpversion~=0.5', 40 | 'coverage~=4.5', 41 | 'PyYAML~=5.1', 42 | 'setuptools~=41.0', 43 | 'flake8~=3.7', 44 | ], 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /braindump/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from braindump.views import BraindumpSession, BraindumpOK, BraindumpNOK, BraindumpIndex, BraindumpPostpone, \ 4 | CardReset, CardExpedite, CardSetArea 5 | 6 | urlpatterns = [ 7 | url(r'^$', 8 | BraindumpIndex.as_view(), 9 | name='braindump-index'), 10 | url(r'^category/(?P[0-9]+)/$', 11 | BraindumpSession.as_view(), 12 | name='braindump-session'), 13 | url(r'^category/(?P[0-9]+)/card/(?P[0-9]+)/ok$', 14 | BraindumpOK.as_view(), 15 | name='braindump-ok'), 16 | url(r'^category/(?P[0-9]+)/card/(?P[0-9]+)/nok$', 17 | BraindumpNOK.as_view(), 18 | name='braindump-nok'), 19 | url(r'^category/(?P[0-9]+)/card/(?P[0-9]+)/postpone/(?P[0-9]+)$', 20 | BraindumpPostpone.as_view(), 21 | name='braindump-postpone'), 22 | url(r'^card/(?P[0-9]+)/reset/$', 23 | CardReset.as_view(), 24 | name='card-reset'), 25 | url(r'^card/(?P[0-9]+)/expedite/$', 26 | CardExpedite.as_view(), 27 | name='card-expedite'), 28 | url(r'^card/(?P[0-9]+)/set-area/(?P[1-6])/$', 29 | CardSetArea.as_view(), 30 | name='card-set-area'), 31 | ] 32 | -------------------------------------------------------------------------------- /categories/urls/gui.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from categories.views.gui import CategoryList, CategoryDetail, CategoryCreate, CategoryUpdate, CategoryDelete 4 | from categories.views.share_contract_gui import CategoryShareContractList, CategoryShareContractRevoke, \ 5 | CategoryShareContractAccept, CategoryShareContractRequest 6 | 7 | urlpatterns = [ 8 | url(r'^$', CategoryList.as_view(), name='category-list'), 9 | url(r'^add/$', CategoryCreate.as_view(), name='category-create'), 10 | url(r'^(?P[0-9]+)/$', CategoryDetail.as_view(), name='category-detail'), 11 | url(r'^(?P[0-9]+)/edit/$', CategoryUpdate.as_view(), name='category-update'), 12 | url(r'^(?P[0-9]+)/delete/$', CategoryDelete.as_view(), name='category-delete'), 13 | url(r'^(?P[0-9]+)/shares/$', CategoryShareContractList.as_view(), name='category-share-contract-list'), 14 | url(r'^(?P[0-9]+)/shares/request/$', 15 | CategoryShareContractRequest.as_view(), 16 | name='category-share-contract-request'), 17 | url(r'^(?P[0-9]+)/shares/(?P[0-9]+)/revoke/$', 18 | CategoryShareContractRevoke.as_view(), 19 | name='category-share-contract-revoke'), 20 | url(r'^shares/(?P[0-9]+)/accept/$', 21 | CategoryShareContractAccept.as_view(), 22 | name='category-share-contract-accept'), 23 | ] 24 | -------------------------------------------------------------------------------- /memodrop/settings/production.py.dist: -------------------------------------------------------------------------------- 1 | """Django production settings (EXAMPLE) 2 | See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 3 | """ 4 | 5 | from .base import * 6 | 7 | print('Loading production settings') 8 | 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = '' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = False 15 | 16 | ALLOWED_HOSTS = [''] 17 | 18 | 19 | # Database 20 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 21 | 22 | DATABASES = { 23 | 'default': { 24 | 'ENGINE': 'django.db.backends.sqlite3', 25 | 'NAME': os.path.join(BASE_DIR, '../../db.sqlite3'), 26 | } 27 | } 28 | 29 | 30 | # Static files (CSS, JavaScript, Images) 31 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 32 | 33 | # STATIC_ROOT = '/absolute/path/to/collected/static/files/' 34 | # STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' 35 | 36 | # Queue 37 | 38 | # Q_CLUSTER = { 39 | # 'redis': { 40 | # 'host': '127.0.0.1', 41 | # 'port': 6379, 42 | # 'db': 0, 43 | # }, 44 | # } 45 | 46 | # ADMINS = [('John', 'john@example.com'), ('Mary', 'mary@example.com')] 47 | 48 | # DEFAULT_FROM_EMAIL = 'webmaster@localhost' 49 | # EMAIL_BACKEND = 'django_sendmail_backend.backends.EmailBackend' 50 | -------------------------------------------------------------------------------- /cards/templates/cards/card_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% load markdown_deux_tags %} 4 | {% block title %}Delete confirmation{% endblock %} 5 | {% block content %} 6 |

Delete confirmation

7 |
8 | {% csrf_token %} 9 |
10 |
11 | {{ card.question|markdown }} 12 | {% if card.hint %} 13 |
14 | {{ card.hint|markdown }} 15 | {% endif %} 16 |
17 | {{ card.answer|markdown }} 18 |
19 |
20 | {% if card.is_shared_with %} 21 |

This card is part of a shared category. If you delete it, no user will be able to access it anymore. This action cannot be undone.

22 | {% endif %} 23 |

Are you sure you want to delete {{ card }} of 24 | category {{ card.category }}? 25 | This action cannot be undone. 26 |

27 | {% buttons %} 28 | 29 | {% endbuttons %} 30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /categories/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | from django.db import IntegrityError 4 | 5 | from categories.exceptions import ShareContractUserIsOwner, ShareContractAlreadyExists, ShareContractUserDoesNotExist 6 | from categories.models import ShareContract 7 | 8 | 9 | class ShareContractForm(forms.Form): 10 | username = forms.CharField(label='Username') 11 | 12 | def create_share_contract(self, category, requester_user): 13 | """Creates a new share contract if the user exists 14 | """ 15 | user_query_set = User.objects.filter(username=self.cleaned_data['username']) 16 | if user_query_set.exists(): 17 | if user_query_set.first() == requester_user: 18 | # Raise exception in case that the target user is the owner of the category (circular dependency): 19 | raise ShareContractUserIsOwner() 20 | try: 21 | return ShareContract.objects.create( 22 | category=category, 23 | user=user_query_set.first(), 24 | ) 25 | except IntegrityError: 26 | # Raise exception in case that this ShareContract already exists (unique key constraint): 27 | raise ShareContractAlreadyExists() 28 | else: 29 | # Raise exception in case that the requested User does not exist: 30 | raise ShareContractUserDoesNotExist() 31 | -------------------------------------------------------------------------------- /categories/templates/categories/category_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %}Delete confirmation{% endblock %} 4 | {% block content %} 5 |

Delete confirmation

6 |
7 | {% csrf_token %} 8 | {% if category.cards.all|length %} 9 | This category contains {{ category.cards.all|length }} card{{ category.cards.all|length|pluralize }}: 10 |
    11 | {% for card in category.cards.all %} 12 |
  • {{ card.question }} 13 |
  • 14 | {% endfor %} 15 |
16 | {% endif %} 17 | {% if category.is_shared_with %} 18 |

It is shared with {{ category.is_shared_with|length }} user{{ category.is_shared_with|length|pluralize }}. No user will be able to access it anymore. This action cannot be undone.

19 | {% endif %} 20 |

Are you sure you want to delete the category {{ category }}? This action cannot be undone.

23 | {% buttons %} 24 | 25 | {% endbuttons %} 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /braindump/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-04-11 08:45 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.utils.timezone 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('cards', '0013_card_postpone_until'), 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='CardPlacement', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('area', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6')], default=1, verbose_name='Area')), 25 | ('last_interaction', models.DateTimeField(auto_now_add=True)), 26 | ('postpone_until', models.DateTimeField(default=django.utils.timezone.now)), 27 | ('card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='card_placements', to='cards.Card')), 28 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 29 | ], 30 | ), 31 | migrations.AlterUniqueTogether( 32 | name='cardplacement', 33 | unique_together=set([('card', 'user')]), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /memodrop/settings/development.py: -------------------------------------------------------------------------------- 1 | """Django development settings 2 | """ 3 | 4 | from .base import * 5 | from ._generate_secret_key import generate_secret_key 6 | from codecs import open 7 | from os import path 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | print('Loading development settings') 12 | 13 | # Quick-start development settings - unsuitable for production 14 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 15 | 16 | 17 | # SECURITY WARNING: keep the secret key used in production secret! 18 | # We're using the SECRET_KEY stored in "development.key", otherwise we're generating a new one: 19 | key_file = path.join(here, 'development.key') 20 | if os.path.exists(key_file): 21 | with open(key_file, 'r') as f: 22 | SECRET_KEY = f.read().strip() 23 | else: 24 | # key_file does not exist: 25 | SECRET_KEY = None 26 | 27 | # Key file does not exist or is empty: 28 | if not SECRET_KEY: 29 | print('Generating new secret key') 30 | SECRET_KEY = generate_secret_key() 31 | with open(key_file, 'w') as f: 32 | f.write(SECRET_KEY) 33 | else: 34 | print('Using the secret key stored in "{}"'.format(key_file)) 35 | 36 | # SECURITY WARNING: don't run with debug turned on in production! 37 | DEBUG = True 38 | 39 | ALLOWED_HOSTS = ['*'] 40 | 41 | 42 | # Database 43 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 44 | 45 | DATABASES = { 46 | 'default': { 47 | 'ENGINE': 'django.db.backends.sqlite3', 48 | 'NAME': os.path.join(BASE_DIR, '../db.sqlite3'), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /categories/fixtures/demo_categories.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - model: categories.Category 3 | pk: 1 4 | fields: 5 | name: Strict Category 6 | description: The first demo category. 7 | mode: 1 8 | owner: 1 9 | - model: categories.Category 10 | pk: 2 11 | fields: 12 | name: Defensive Category 13 | description: The second demo category. 14 | mode: 2 15 | owner: 1 16 | - model: categories.Category 17 | pk: 3 18 | fields: 19 | name: Category With One Single Card 20 | description: The third demo category. 21 | mode: 1 22 | owner: 1 23 | - model: categories.Category 24 | pk: 4 25 | fields: 26 | name: Empty Category 27 | description: The fourth demo category. 28 | mode: 2 29 | owner: 1 30 | - model: categories.Category 31 | pk: 5 32 | fields: 33 | name: Markdown Stuff 34 | description: > 35 | **Strong** 36 | 37 | 38 | [Link to localhost](http://localhost/) 39 | mode: 1 40 | owner: 1 41 | - model: categories.Category 42 | pk: 6 43 | fields: 44 | name: Category of user number 2 45 | description: This category belongs to user number 2. 46 | mode: 1 47 | owner: 2 48 | - model: categories.Category 49 | pk: 7 50 | fields: 51 | name: Shared Category 52 | description: This category is owned by user number 1 and shared with user number 2 53 | mode: 1 54 | owner: 1 55 | - model: categories.Category 56 | pk: 8 57 | fields: 58 | name: Empty Shared Category 59 | description: This category is owned by user number 1, but there is an open share request for user number 2 60 | mode: 1 61 | owner: 1 62 | -------------------------------------------------------------------------------- /memodrop/static/fa-4.7.0/LICENSE: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OLF license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OLF, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** 35 | -------------------------------------------------------------------------------- /memodrop/static/fonts/LICENSE: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OLF license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OLF, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** 35 | -------------------------------------------------------------------------------- /memodrop/static/jquery-3.2.1/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /braindump/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models import signals 2 | from django.dispatch import receiver 3 | from django_q.tasks import async_task, async_chain 4 | 5 | from braindump.models import CardPlacement 6 | from cards.models import Card 7 | from categories.models import ShareContract 8 | from categories.signals import share_contract_accepted, share_contract_revoked 9 | 10 | 11 | @receiver(signals.post_save, sender=Card) 12 | def create_card_placement_for_new_card(instance, created, **kwargs): 13 | """Creates a card placement for a recently created card 14 | """ 15 | if created: 16 | CardPlacement.objects.create( 17 | card=instance, 18 | user=instance.category.owner, 19 | ) 20 | 21 | for share_contract in instance.category.share_contracts.filter(accepted=True): 22 | CardPlacement.objects.create( 23 | card=instance, 24 | user=share_contract.user, 25 | ) 26 | 27 | 28 | @receiver(share_contract_accepted, sender=ShareContract) 29 | def share_contract_accepted(share_contract, **kwargs): 30 | """Signal handler for share contracts that have been accepted 31 | """ 32 | async_task('braindump.tasks.create_card_placements_for_shared_category', share_contract) 33 | 34 | 35 | @receiver(share_contract_revoked, sender=ShareContract) 36 | def share_contract_revoked(share_contract, **kwargs): 37 | """Signal handler for shared contracts that have been deleted 38 | """ 39 | # Do this only if the share contract was accepted: 40 | if share_contract.accepted: 41 | async_chain([ 42 | ('braindump.tasks.create_independent_category', (share_contract,)), 43 | ('braindump.tasks.delete_revoked_share_contract', (share_contract,)), 44 | ]) 45 | -------------------------------------------------------------------------------- /authentication/management/commands/check_userguisettings_consistency.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.contrib.auth.models import User 4 | from django.core.management import BaseCommand 5 | 6 | from authentication.models import UserGUISettings 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Checks the consistency of user specific GUI settings' 11 | 12 | def add_arguments(self, parser): 13 | """Argument handle 14 | """ 15 | parser.add_argument('--repair', help='Try to repair inconsistencies', action='store_true') 16 | 17 | def handle(self, *args, **options): 18 | """Command handle 19 | """ 20 | users = User.objects.all() 21 | users_ok = list() 22 | users_nok = list() 23 | for user in users: 24 | user_gui_settings_item = UserGUISettings.objects.filter(user=user) 25 | if user_gui_settings_item.exists(): 26 | users_ok.append(user) 27 | else: 28 | users_nok.append(user) 29 | 30 | for user in users_ok: 31 | self.stdout.write(self.style.SUCCESS('%s OK' % user)) 32 | for user in users_nok: 33 | self.stdout.write(self.style.ERROR('%s NOT OK' % user)) 34 | 35 | if options['repair']: 36 | self.stdout.write('') 37 | self.stdout.write('Trying to fix the inconsistencies') 38 | for user in users_nok: 39 | user_gui_settings_item = UserGUISettings(user=user) 40 | user_gui_settings_item.save() 41 | self.stdout.write(self.style.SUCCESS('Created #%d for user %s' % (user_gui_settings_item.id, user))) 42 | else: 43 | if users_nok: 44 | self.stdout.write('') 45 | self.stdout.write('Try to fix the inconsistencies with --repair') 46 | sys.exit(1) 47 | -------------------------------------------------------------------------------- /cards/templates/cards/card_list.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load area_rating %} 3 | {% load card_controls %} 4 | {% load bootstrap4 %} 5 | {% block title %}Cards{% endblock %} 6 | {% block content %} 7 |

8 | Cards 9 |
10 | Create card 11 |
12 |

13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for card_placement in object_list %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% empty %} 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
QuestionAreaCategory
{{ card_placement.card.question|truncatechars:128 }}{% if card_placement.postponed %} (postponed){% endif %}{% area_rating card_placement.area %}{{ card_placement.card.category.name|truncatechars:32 }}{% if user in card_placement.card.category.is_shared_with %} (shared){% endif %}{% card_controls card_placement %}
No cards have been created yet.
38 |
39 | 40 | {% bootstrap_pagination page_obj %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /memodrop/urls.py: -------------------------------------------------------------------------------- 1 | """memodrop URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | import watchman.views 17 | from django.conf.urls import url, include 18 | from django.contrib import admin 19 | from django.urls import reverse_lazy 20 | from django.views.generic import RedirectView 21 | from rest_framework.authtoken import views as auth_token_views 22 | 23 | urlpatterns = [ 24 | url(r'^$', 25 | RedirectView.as_view(url=reverse_lazy('braindump-index'), permanent=False), 26 | name='index'), 27 | url(r'^authentication/', include('authentication.urls.gui')), 28 | url(r'^admin/', admin.site.urls), 29 | url(r'^admin/health/$', watchman.views.bare_status), 30 | url(r'^api/(?P(v1))/auth/', include('rest_framework.urls')), 31 | url(r'^api/(?P(v1))/authentication/', include('authentication.urls.api')), 32 | url(r'^api/(?P(v1))/auth-token/', auth_token_views.obtain_auth_token), 33 | url(r'^api/(?P(v1))/categories/', include('categories.urls.api')), 34 | url(r'^api/(?P(v1))/cards/', include('cards.urls.api')), 35 | url(r'^braindump/', include('braindump.urls')), 36 | url(r'^cards/', include('cards.urls.gui')), 37 | url(r'^categories/', include('categories.urls.gui')), 38 | ] 39 | -------------------------------------------------------------------------------- /cards/templates/cards/card_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load markdown_deux_tags %} 3 | {% load area_rating %} 4 | {% block title %}{{ card }}{% endblock %} 5 | {% block content %} 6 |
7 |

8 |
9 | Go to category 10 |
11 | {{ card.category.name }} 12 |

13 |
14 |
15 | Card #{{ card.pk }} 16 |
17 | {% area_rating card_placement.area %} 18 |
19 |
20 |
21 | {{ card.question|markdown }} 22 | {% if card.hint %} 23 |
24 |
25 | {{ card.hint|markdown }} 26 |
27 | {% endif %} 28 |
29 |
30 | {{ card.answer|markdown }} 31 |
32 |
33 | 40 |
41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /authentication/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block custom_stylesheet_links %} 6 | 7 | {% endblock %} 8 | 9 | {% block custom_body_classes %} 10 | text-center 11 | {% endblock %} 12 | 13 | {% block custom_javascript_initialization_tags %} 14 | 17 | {% endblock %} 18 | 19 | {% block body %} 20 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /cards/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | 4 | 5 | class CardOwnerManager(models.Manager): 6 | def all(self, user): 7 | """Returns all cards belonging to the category's owner 8 | """ 9 | return self.filter(category__owner=user).all() 10 | 11 | def get(self, user, *args, **kwargs): 12 | """Returns a card belonging to the category's owner 13 | """ 14 | return self.filter(category__owner=user).get(*args, **kwargs) 15 | 16 | 17 | class CardSharedManager(models.Manager): 18 | def all(self, user): 19 | """Returns all cards belonging to the category's owner 20 | """ 21 | return self.filter(category__share_contracts__user=user, category__share_contracts__accepted=True).all() 22 | 23 | def get(self, user, *args, **kwargs): 24 | """Returns a card belonging to the category's owner 25 | """ 26 | return self.filter(category__share_contracts__user=user, category__share_contracts__accepted=True).get(*args, 27 | **kwargs) 28 | 29 | 30 | class Card(models.Model): 31 | AREA_CHOICES = ( 32 | (1, '1'), 33 | (2, '2'), 34 | (3, '3'), 35 | (4, '4'), 36 | (5, '5'), 37 | (6, '6'), 38 | ) 39 | question = models.TextField(verbose_name='Question') 40 | answer = models.TextField(verbose_name='Answer') 41 | hint = models.TextField(blank=True, verbose_name='Hint') 42 | category = models.ForeignKey('categories.Category', on_delete=models.CASCADE, related_name='cards') 43 | objects = models.Manager() 44 | owned_objects = CardOwnerManager() 45 | shared_objects = CardSharedManager() 46 | 47 | def __str__(self): 48 | return 'Card #{}'.format(self.pk) 49 | 50 | def get_absolute_url(self): 51 | return reverse('card-detail', kwargs={'pk': self.pk}) 52 | 53 | def is_shared_with(self): 54 | """Returns a list of users this card is shared with 55 | """ 56 | return [s.user for s in self.category.share_contracts.filter(accepted=True)] 57 | -------------------------------------------------------------------------------- /categories/templates/categories/sharecontract_list.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %}Users with access to "{{ category }}"{% endblock %} 4 | {% block content %} 5 |

6 | Users with access to "{{ category }}" 7 |
8 | Invite users 9 |
10 |

11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for share_contract in object_list %} 21 | 22 | 29 | 38 | 39 | {% empty %} 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 |
User
23 | {% if share_contract.user.first_name and share_contract.user.last_name %} 24 | {{ share_contract.user.first_name }} {{ share_contract.user.last_name }} 25 | {% else %} 26 | {{ share_contract.user }} 27 | {% endif %} 28 | 30 |
31 | {% if not share_contract.revoked %} 32 | Revoke 33 | {% else %} 34 | will be revoked 35 | {% endif %} 36 |
37 |
This category has not been shared yet.
46 |
47 | 48 | {% bootstrap_pagination page_obj %} 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /categories/templates/categories/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %}Categories{% endblock %} 4 | {% block content %} 5 |

6 | Categories 7 |
8 | Create category 9 |
10 |

11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for category in category_list %} 23 | 24 | 30 | 31 | 32 | 39 | 40 | {% empty %} 41 | 42 | 43 | 44 | {% endfor %} 45 | 46 |
CategoryCardsMode
25 | {{ category.name }} 26 | {% if user in category.is_shared_with %} 27 | (shared with me) 28 | {% endif %} 29 | {{ category.cards.all|length }}{{ category.get_mode_display }} Mode 33 | 38 |
No categories have been created yet.
47 |
48 | 49 | {% bootstrap_pagination page_obj %} 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /authentication/views/gui.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import get_object_or_404 2 | 3 | from authentication.models import UserGUISettings 4 | from categories.models import Category 5 | from django.contrib import messages 6 | from django.contrib.auth.mixins import LoginRequiredMixin 7 | from django.urls import reverse, reverse_lazy 8 | from django.views.generic import RedirectView, TemplateView, UpdateView 9 | 10 | 11 | class PasswordChangeDoneView(RedirectView): 12 | """Renders the password change done view 13 | """ 14 | permanent = False 15 | 16 | def get_redirect_url(self, *args, **kwargs): 17 | messages.info(self.request, 'Your password was changed.') 18 | return reverse('index') 19 | 20 | 21 | class ProfileView(LoginRequiredMixin, TemplateView): 22 | """Renders the user's profile 23 | """ 24 | template_name = 'auth/profile.html' 25 | 26 | def get_context_data(self, **kwargs): 27 | owned_category_count = Category.owned_objects.all(self.request.user).count() 28 | shared_category_count = Category.shared_objects.all(self.request.user).count() 29 | total_category_count = owned_category_count + shared_category_count 30 | 31 | owned_card_count = 0 32 | for category in Category.owned_objects.all(self.request.user): 33 | owned_card_count += category.cards.count() 34 | 35 | shared_card_count = 0 36 | for category in Category.shared_objects.all(self.request.user): 37 | shared_card_count += category.cards.count() 38 | 39 | total_card_count = owned_card_count + shared_card_count 40 | 41 | context = { 42 | 'owned_category_count': owned_category_count, 43 | 'shared_category_count': shared_category_count, 44 | 'total_category_count': total_category_count, 45 | 'owned_card_count': owned_card_count, 46 | 'shared_card_count': shared_card_count, 47 | 'total_card_count': total_card_count, 48 | } 49 | 50 | return context 51 | 52 | 53 | class UserGUISettingsUpdate(LoginRequiredMixin, UpdateView): 54 | """Update user specific GUI settings 55 | """ 56 | fields = ['enable_markdown_editor'] 57 | success_url = reverse_lazy('authentication-user-gui-settings-update') 58 | 59 | def get_object(self, queryset=None): 60 | return get_object_or_404(UserGUISettings, user=self.request.user) 61 | 62 | def form_valid(self, form): 63 | messages.success(self.request, 'Settings updated.') 64 | return super(UserGUISettingsUpdate, self).form_valid(form) 65 | -------------------------------------------------------------------------------- /cards/fixtures/demo_cards.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - model: cards.Card 3 | pk: 1 4 | fields: 5 | question: Question 1 6 | answer: Answer 1 7 | hint: Hint 1 8 | category: 1 9 | - model: cards.Card 10 | pk: 2 11 | fields: 12 | question: Question 2 13 | answer: Answer 2 14 | hint: Hint 2 15 | category: 1 16 | - model: cards.Card 17 | pk: 3 18 | fields: 19 | question: Question 3 20 | answer: Answer 3 21 | hint: Hint 3 22 | category: 1 23 | - model: cards.Card 24 | pk: 4 25 | fields: 26 | question: Question 4 27 | answer: Answer 4 28 | hint: Hint 4 29 | category: 1 30 | - model: cards.Card 31 | pk: 5 32 | fields: 33 | question: Question 5 34 | answer: Answer 5 35 | hint: Hint 5 36 | category: 1 37 | - model: cards.Card 38 | pk: 6 39 | fields: 40 | question: Question 6 41 | answer: Answer 6 42 | hint: Hint 6 43 | category: 1 44 | - model: cards.Card 45 | pk: 7 46 | fields: 47 | question: Question 1 48 | answer: Answer 1 49 | hint: Hint 1 50 | category: 2 51 | - model: cards.Card 52 | pk: 8 53 | fields: 54 | question: Question 2 55 | answer: Answer 2 56 | hint: Hint 2 57 | category: 2 58 | - model: cards.Card 59 | pk: 9 60 | fields: 61 | question: Question 3 62 | answer: Answer 3 63 | hint: Hint 3 64 | category: 2 65 | - model: cards.Card 66 | pk: 10 67 | fields: 68 | question: Question 4 69 | answer: Answer 4 70 | hint: Hint 4 71 | category: 2 72 | - model: cards.Card 73 | pk: 11 74 | fields: 75 | question: Question 5 76 | answer: Answer 5 77 | hint: Hint 5 78 | category: 2 79 | - model: cards.Card 80 | pk: 12 81 | fields: 82 | question: Question 6 83 | answer: Answer 6 84 | hint: Hint 6 85 | category: 2 86 | - model: cards.Card 87 | pk: 13 88 | fields: 89 | question: Question without hint 90 | answer: Answer without hint 91 | category: 3 92 | - model: cards.Card 93 | pk: 14 94 | fields: 95 | question: > 96 | 1. First 97 | 98 | 2. Second 99 | 100 | 3. Third 101 | answer: > 102 | - First 103 | 104 | - Second 105 | 106 | - Third 107 | hint: > 108 | # Headline 1 109 | 110 | ## Headline 2 111 | 112 | ### Headline 3 113 | category: 5 114 | - model: cards.Card 115 | pk: 15 116 | fields: 117 | question: First question of user number 2 118 | answer: First answer of user number 2 119 | category: 6 120 | - model: cards.Card 121 | pk: 16 122 | fields: 123 | question: Shared card 124 | answer: Shared answer 125 | category: 7 126 | -------------------------------------------------------------------------------- /braindump/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from braindump.models import CardPlacement 4 | from cards.models import Card 5 | from categories.models import Category 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def create_card_placements_for_shared_category(share_contract): 12 | """Creates card placements for a recently accepted share contract 13 | """ 14 | for card in share_contract.category.cards.all(): 15 | logger.info('Creating card placement for share contract #{}, card #{} and user #{}'.format( 16 | share_contract.pk, card.pk, share_contract.user.pk 17 | )) 18 | CardPlacement.objects.create( 19 | card=card, 20 | user=share_contract.user, 21 | ) 22 | 23 | 24 | def create_independent_category(share_contract): 25 | """Duplicates an existing category with its cards and moves all necessary card placements 26 | """ 27 | # Duplicate category: 28 | logger.info('Duplicating category #{}'.format(share_contract.category.pk)) 29 | new_category = Category.objects.get(pk=share_contract.category.pk) 30 | new_category.pk = None 31 | new_category.owner = share_contract.user 32 | new_category.save() 33 | logger.debug('New category is #{}'.format(share_contract.category.pk, new_category.pk)) 34 | 35 | for card in share_contract.category.cards.all(): 36 | # Duplicate card: 37 | logger.info('Duplicating card #{}'.format(card.pk)) 38 | new_card = Card.objects.get(pk=card.pk) 39 | new_card.pk = None 40 | new_card.category = new_category 41 | new_card.save() # this will be creating a new card placement automatically 42 | logger.debug('New card is #{}'.format(card.pk, new_card.pk)) 43 | 44 | # Remove the auto-generated card placement: 45 | automatic_card_placement = CardPlacement.card_user_objects.get(card=new_card, user=share_contract.user) 46 | logger.debug('Removing auto-generated card placement #{}'.format(automatic_card_placement.pk)) 47 | automatic_card_placement.delete() 48 | 49 | # Move existing card placement to the new card: 50 | card_placement = CardPlacement.card_user_objects.get(card=card, user=share_contract.user) 51 | logger.debug('Moving existing card placement #{} to recently created card #{}'.format( 52 | card_placement.pk, new_card.pk 53 | )) 54 | card_placement.card = new_card 55 | card_placement.save() 56 | 57 | 58 | def delete_revoked_share_contract(share_contract): 59 | """Deletes a revoked share contract 60 | """ 61 | if share_contract.accepted and share_contract.revoked: 62 | logger.info('Deleting revoked share contract #{}'.format(share_contract.pk)) 63 | share_contract.delete() 64 | else: 65 | logger.warning('Refuse to delete revoked share contract #{}, because it has not been accepted yet'.format( 66 | share_contract.pk 67 | )) 68 | -------------------------------------------------------------------------------- /memodrop/templates/main_authorized.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | {% load bootstrap4 %} 5 | {% load font_awesome %} 6 | 7 | {% block custom_javascript_initialization_tags %} 8 | 11 | {% endblock %} 12 | 13 | {% block body %} 14 | 59 |
60 | {% bootstrap_messages %} 61 | {% block content %}{% endblock %} 62 |
63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /braindump/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.utils import timezone 4 | 5 | 6 | class CardPlacementUserManager(models.Manager): 7 | def all(self, user): 8 | """Returns all card placements belonging to the user 9 | """ 10 | return self.filter(user=user).all() 11 | 12 | def get(self, user, *args, **kwargs): 13 | """Returns a card placement belonging to the user 14 | """ 15 | return self.filter(user=user).get(*args, **kwargs) 16 | 17 | 18 | class CardPlacementCardManager(models.Manager): 19 | def all(self, card): 20 | """Returns all card placements belonging to a card 21 | """ 22 | return self.filter(card=card).all() 23 | 24 | def get(self, card, *args, **kwargs): 25 | """Returns a card placement belonging to a card 26 | """ 27 | return self.filter(card=card).get(*args, **kwargs) 28 | 29 | 30 | class CardPlacementCardUserManager(models.Manager): 31 | def get(self, card, user, *args, **kwargs): 32 | """Returns a card placement belonging to a card and the user 33 | """ 34 | return self.filter(card=card, user=user).get(*args, **kwargs) 35 | 36 | 37 | class CardPlacement(models.Model): 38 | AREA_CHOICES = ( 39 | (1, '1'), 40 | (2, '2'), 41 | (3, '3'), 42 | (4, '4'), 43 | (5, '5'), 44 | (6, '6'), 45 | ) 46 | area = models.IntegerField(default=1, choices=AREA_CHOICES, verbose_name='Area') 47 | card = models.ForeignKey('cards.Card', on_delete=models.CASCADE, related_name='card_placements') 48 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 49 | last_interaction = models.DateTimeField(auto_now_add=True, blank=True, editable=False) 50 | postpone_until = models.DateTimeField(default=timezone.now, blank=True) 51 | objects = models.Manager() 52 | user_objects = CardPlacementUserManager() 53 | card_objects = CardPlacementCardManager() 54 | card_user_objects = CardPlacementCardUserManager() 55 | 56 | class Meta: 57 | unique_together = (('card', 'user'),) 58 | ordering = ['area'] 59 | indexes = [ 60 | models.Index(fields=['user']), 61 | models.Index(fields=['card']), 62 | ] 63 | 64 | def move_forward(self): 65 | """Increase the area 66 | """ 67 | if self.area < 6: 68 | self.area += 1 69 | self.save() 70 | 71 | def move_backward(self): 72 | """Decrease the area 73 | """ 74 | if self.area > 1: 75 | self.area -= 1 76 | self.save() 77 | 78 | def reset(self): 79 | """Set card to area 1 80 | """ 81 | self.area = 1 82 | self.save() 83 | 84 | def set_last_interaction(self, last_interaction=False): 85 | """Set date and time of last interaction 86 | """ 87 | if not last_interaction: 88 | last_interaction = timezone.now() 89 | self.last_interaction = last_interaction 90 | self.save() 91 | 92 | def postponed(self): 93 | """Return true if card has been postponed 94 | """ 95 | if self.postpone_until > timezone.now(): 96 | return True 97 | else: 98 | return False 99 | 100 | def expedite(self): 101 | """Reset postpone marker 102 | """ 103 | self.postpone_until = timezone.now() 104 | self.save() 105 | -------------------------------------------------------------------------------- /authentication/templates/auth/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load bootstrap4 %} 3 | {% block title %} 4 | {% if request.user.first_name %}{{ request.user.first_name }}{% else %}{{ request.user.username }}{% endif %}'s profile 5 | {% endblock %} 6 | {% block content %} 7 |
8 |
9 |
10 | {% if request.user.first_name %} 11 |

{{ request.user.first_name }} {{ request.user.last_name }}

12 |

{{ request.user.username }}

13 | {% else %} 14 |

{{ request.user.username }}

15 | {% endif %} 16 |

{{ request.user.email }}

17 | 21 |
22 |
23 |

Statistics

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 | 59 |
CardsCategories
Number of cards you own{{ owned_card_count }}
Number of categories you own{{ owned_category_count }}
Number of cards which have been shared with you{{ shared_card_count }}
Number of categories which have been shared with you{{ shared_category_count }}
Σ{{ total_card_count }}{{ total_category_count }}
60 | 61 | {% if request.user.auth_token %} 62 |

API access

63 |

64 | Your authentication token is: 65 | 66 | {{ request.user.auth_token.key }} 67 | 68 |

69 | {% endif %} 70 |
71 |
72 |
73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /categories/views/gui.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.urls import reverse_lazy 4 | from django.views.generic.detail import DetailView 5 | from django.views.generic.edit import CreateView, UpdateView, DeleteView 6 | from django.views.generic.list import ListView 7 | 8 | from braindump.models import CardPlacement 9 | from categories.models import Category 10 | 11 | 12 | class CategoryBelongsOwnerMixin: 13 | """Mixin that returns all categories owned by the authorized user 14 | """ 15 | def get_queryset(self): 16 | return Category.owned_objects.all(self.request.user) 17 | 18 | 19 | class CategoryBelongsUserMixin: 20 | """Mixin that returns all categories belonging to the authorized user 21 | """ 22 | def get_queryset(self): 23 | owned_category_list = Category.owned_objects.all(self.request.user) 24 | shared_category_list = Category.shared_objects.all(self.request.user) 25 | return owned_category_list | shared_category_list 26 | 27 | 28 | class CategoryList(LoginRequiredMixin, CategoryBelongsUserMixin, ListView): 29 | """List all categories 30 | """ 31 | paginate_by = 25 32 | paginate_orphans = 5 33 | 34 | 35 | class CategoryDetail(LoginRequiredMixin, CategoryBelongsUserMixin, DetailView): 36 | """Show detailed information about a category 37 | """ 38 | def get_context_data(self, **kwargs): 39 | context = super(CategoryDetail, self).get_context_data(**kwargs) 40 | context['card_placements'] = CardPlacement.user_objects.all(self.request.user).filter( 41 | card__category=self.object.id, 42 | ).all() 43 | 44 | for i in range(1, 7): 45 | context['area{}'.format(i)] = CardPlacement.user_objects.all(self.request.user).filter( 46 | card__category=self.object.id, 47 | area=i, 48 | ).all() 49 | 50 | return context 51 | 52 | 53 | class CategoryCreate(LoginRequiredMixin, CategoryBelongsUserMixin, CreateView): 54 | """Create a new category 55 | """ 56 | model = Category 57 | fields = ['name', 'description', 'mode'] 58 | template_name_suffix = '_create_form' 59 | 60 | def get_form(self, form_class=None): 61 | form = super().get_form(form_class) 62 | form.fields['mode'].help_text = """ 63 | Strict Mode: Incorrectly answered cards are moved back to the first area.
64 | Defensive Mode: Incorrectly answered cards are moved back to the previous area. 65 | """ 66 | return form 67 | 68 | def form_valid(self, form): 69 | messages.success(self.request, 'Category created.') 70 | form.instance.owner = self.request.user 71 | return super(CategoryCreate, self).form_valid(form) 72 | 73 | 74 | class CategoryUpdate(LoginRequiredMixin, CategoryBelongsOwnerMixin, UpdateView): 75 | """Update a category 76 | """ 77 | fields = ['name', 'description', 'mode'] 78 | template_name_suffix = '_update_form' 79 | 80 | def get_form(self, form_class=None): 81 | form = super().get_form(form_class) 82 | form.fields['mode'].help_text = """ 83 | Strict Mode: Incorrectly answered cards are moved back to the first area.
84 | Defensive Mode: Incorrectly answered cards are moved back to the previous area. 85 | """ 86 | return form 87 | 88 | def form_valid(self, form): 89 | messages.success(self.request, 'Category updated.') 90 | return super(CategoryUpdate, self).form_valid(form) 91 | 92 | 93 | class CategoryDelete(LoginRequiredMixin, CategoryBelongsOwnerMixin, DeleteView): 94 | """Delete a category 95 | """ 96 | success_url = reverse_lazy('category-list') 97 | 98 | def delete(self, request, *args, **kwargs): 99 | messages.success(self.request, 'Category deleted.') 100 | return super(CategoryDelete, self).delete(request, *args, **kwargs) 101 | -------------------------------------------------------------------------------- /braindump/management/commands/check_card_placement_consistency.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.management import BaseCommand 4 | 5 | from braindump.models import CardPlacement 6 | from categories.models import Category, ShareContract 7 | 8 | 9 | class CheckResult: 10 | def __init__(self, category, ok, nok): 11 | self.category = category 12 | self.ok = ok 13 | self.nok = nok 14 | 15 | 16 | class Command(BaseCommand): 17 | help = 'Checks the consistency of card placements' 18 | 19 | def add_arguments(self, parser): 20 | """Argument handle 21 | """ 22 | parser.add_argument('--repair', help='Try to repair inconsistencies', action='store_true') 23 | 24 | def handle(self, *args, **options): 25 | """Command handle 26 | """ 27 | self.stdout.write('') 28 | self.stdout.write('----------------------------------------') 29 | self.stdout.write('Checking card placements for category owners') 30 | check_category_owners = self._check_category_owners() 31 | self._format_category_results(check_category_owners) 32 | 33 | self.stdout.write('') 34 | self.stdout.write('----------------------------------------') 35 | self.stdout.write('Checking card placements for users of shared categories') 36 | check_category_users = self._check_category_users() 37 | self._format_category_results(check_category_users) 38 | 39 | if options['repair']: 40 | self.stdout.write('') 41 | self.stdout.write('----------------------------------------') 42 | self.stdout.write('Trying to fix the inconsistencies') 43 | for result in check_category_owners + check_category_users: 44 | for nok in result.nok: 45 | card_placement = CardPlacement.objects.create( 46 | card=nok[0], 47 | user=nok[1], 48 | ) 49 | self.stdout.write(self.style.SUCCESS('Created %s for result %s' % (card_placement, nok))) 50 | else: 51 | self.stdout.write('') 52 | inconsistency_detected = False 53 | for result in check_category_owners + check_category_users: 54 | if result.nok: 55 | inconsistency_detected = True 56 | self.stdout.write('Try to fix the inconsistencies with --repair') 57 | break 58 | 59 | if inconsistency_detected: 60 | sys.exit(1) 61 | 62 | def _format_category_results(self, results): 63 | """Print formatted category results 64 | """ 65 | for result in results: 66 | self.stdout.write('#%d %s' % (result.category.id, result.category)) 67 | self.stdout.write(self.style.SUCCESS(' - %dx OK' % len(result.ok))) 68 | for nok in result.nok: 69 | self.stdout.write(self.style.ERROR(' - Missing card placement: {}'.format(nok))) 70 | 71 | def _check_category_owners(self): 72 | """Iterates through all categories and checks its owners 73 | """ 74 | resp = list() 75 | for category in Category.objects.all(): 76 | ok, nok = self._check_card_placement(category, category.owner) 77 | resp.append(CheckResult(category, ok, nok)) 78 | return resp 79 | 80 | def _check_category_users(self): 81 | """Iterates through all share contracts and checks its users 82 | """ 83 | resp = list() 84 | for share_contract in ShareContract.objects.all(): 85 | ok, nok = self._check_card_placement(share_contract.category, share_contract.user) 86 | resp.append(CheckResult(share_contract.category, ok, nok)) 87 | return resp 88 | 89 | def _check_card_placement(self, category, user): 90 | """Iterates through all cards and looks for card placements 91 | """ 92 | ok = list() 93 | nok = list() 94 | for card in category.cards.all(): 95 | if CardPlacement.objects.filter(card=card, user=user).exists(): 96 | ok.append((card, user)) 97 | else: 98 | nok.append((card, user)) 99 | return ok, nok 100 | -------------------------------------------------------------------------------- /memodrop/settings/base.py: -------------------------------------------------------------------------------- 1 | """Django base settings 2 | """ 3 | 4 | import os 5 | 6 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | 9 | 10 | # Application definition 11 | 12 | INSTALLED_APPS = [ 13 | 'authentication.apps.AuthenticationConfig', 14 | 'braindump.apps.BraindumpConfig', 15 | 'categories.apps.CategoriesConfig', 16 | 'cards.apps.CardsConfig', 17 | 'django.contrib.admin', 18 | 'django.contrib.auth', 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.sessions', 21 | 'django.contrib.messages', 22 | 'django.contrib.staticfiles', 23 | 'django_q', 24 | 'bootstrap4', 25 | 'fa', 26 | 'markdown_deux', 27 | 'rest_framework', 28 | 'rest_framework.authtoken', 29 | 'watchman', 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 | ] 41 | 42 | ROOT_URLCONF = 'memodrop.urls' 43 | 44 | TEMPLATES = [ 45 | { 46 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 47 | 'DIRS': [ 48 | os.path.join(BASE_DIR, 'templates'), 49 | ], 50 | 'APP_DIRS': True, 51 | 'OPTIONS': { 52 | 'context_processors': [ 53 | 'django.template.context_processors.debug', 54 | 'django.template.context_processors.request', 55 | 'django.contrib.auth.context_processors.auth', 56 | 'django.contrib.messages.context_processors.messages', 57 | ], 58 | }, 59 | }, 60 | ] 61 | 62 | WSGI_APPLICATION = 'memodrop.wsgi.application' 63 | 64 | 65 | # Password validation 66 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 67 | 68 | AUTH_PASSWORD_VALIDATORS = [ 69 | { 70 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 71 | }, 72 | { 73 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 74 | }, 75 | { 76 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 77 | }, 78 | { 79 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 80 | }, 81 | ] 82 | 83 | 84 | # Internationalization 85 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 86 | 87 | LANGUAGE_CODE = 'en-us' 88 | 89 | # TIME_ZONE = 'UTC' 90 | 91 | USE_I18N = True 92 | 93 | USE_L10N = True 94 | 95 | USE_TZ = True 96 | 97 | 98 | # Static files (CSS, JavaScript, Images) 99 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 100 | 101 | STATICFILES_DIRS = [ 102 | os.path.join(BASE_DIR, 'static'), 103 | ] 104 | STATIC_URL = '/static/' 105 | 106 | 107 | # Queue 108 | 109 | Q_CLUSTER = { 110 | 'sync': True, 111 | 'orm': 'default', 112 | } 113 | 114 | 115 | # Bootstrap 116 | 117 | BOOTSTRAP4 = { 118 | 'base_url': STATIC_URL + 'bootstrap-4.0.0/', 119 | 'css_url': STATIC_URL + 'bootstrap-4.0.0/bootstrap.min.css', 120 | 'popper_url': STATIC_URL + 'popper-1.12.9/popper.min.js', 121 | 'javascript_url': STATIC_URL + 'bootstrap-4.0.0/bootstrap.min.js', 122 | } 123 | 124 | 125 | # Font Awesome 126 | 127 | FONT_AWESOME = { 128 | 'url': STATIC_URL + 'fa-4.7.0/font-awesome.min.css', 129 | } 130 | 131 | 132 | # REST Framework 133 | 134 | REST_FRAMEWORK = { 135 | 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', 136 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 137 | 'rest_framework.authentication.TokenAuthentication', 138 | 'rest_framework.authentication.SessionAuthentication', 139 | ), 140 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 141 | 'PAGE_SIZE': 30, 142 | } 143 | 144 | 145 | # Auth 146 | 147 | LOGIN_URL = 'authentication-login' 148 | LOGIN_REDIRECT_URL = 'index' 149 | 150 | 151 | # Braindump 152 | 153 | BRAINDUMP_MAX_POSTPONE_SECONDS = 3600 154 | 155 | 156 | # User specific GUI settings (defaults) 157 | 158 | USER_GUI_SETTINGS_DEFAULTS = { 159 | 'enable_markdown_editor': True, 160 | } 161 | -------------------------------------------------------------------------------- /cards/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.test import TestCase, Client 4 | from django.urls import reverse 5 | 6 | from cards.models import Card 7 | from categories.models import Category, ShareContract 8 | 9 | 10 | class CardTestCase(TestCase): 11 | def setUp(self): 12 | """Set up test scenario 13 | """ 14 | self.test_user = User.objects.create_user('test') 15 | self.test_category = Category.objects.create(name='Category 1', description='Description 1', 16 | owner=self.test_user) 17 | self.client = Client() 18 | self.client.force_login(self.test_user) 19 | 20 | self.foreign_test_user = User.objects.create_user('card foreigner') 21 | 22 | def _create_test_card(self, suffix='', category=False): 23 | """Create a single test card 24 | """ 25 | if not category: 26 | category = self.test_category 27 | card = Card.objects.create( 28 | question='Question'.format(suffix), 29 | answer='Answer'.format(suffix), 30 | hint='Hint'.format(suffix), 31 | category=category, 32 | ) 33 | return card 34 | 35 | def test_list(self): 36 | """Test if the card list is displayed successfully 37 | """ 38 | url = reverse('card-list') 39 | response = self.client.get(url) 40 | self.assertEqual(response.status_code, 200) 41 | 42 | def test_detail(self): 43 | """Test if the card list is displayed sucessfully 44 | """ 45 | test_card = self._create_test_card() 46 | url = reverse('card-detail', args=(test_card.pk,)) 47 | response = self.client.get(url) 48 | self.assertEqual(response.status_code, 200) 49 | 50 | def test_foreign_card_detail(self): 51 | """Test if the user has no access to foreign cards 52 | """ 53 | test_user = User.objects.create_user('foreigner') 54 | test_category = Category.objects.create( 55 | name='Category 1337', 56 | description='Description 1337', 57 | owner=test_user 58 | ) 59 | test_card = self._create_test_card(category=test_category) 60 | url = reverse('card-detail', args=(test_card.pk,)) 61 | 62 | foreign_client = Client() 63 | foreign_client.force_login(test_user) 64 | foreign_response = foreign_client.get(url) 65 | self.assertEqual(foreign_response.status_code, 200) 66 | 67 | response = self.client.get(url) 68 | self.assertEqual(response.status_code, 404) 69 | 70 | def test_delete(self): 71 | """Test if the "Delete" button works 72 | """ 73 | test_card = self._create_test_card() 74 | url = reverse('card-delete', args=(test_card.pk,)) 75 | response = self.client.post(url) 76 | self.assertEqual(response.status_code, 302) 77 | 78 | with self.assertRaises(ObjectDoesNotExist): 79 | Card.objects.get(pk=test_card.pk) 80 | 81 | def test_not_shared_card(self): 82 | """Test if a not shared card is actually not shared 83 | """ 84 | test_category = Category.objects.create(name='Category', description='Description', owner=self.test_user) 85 | test_card = self._create_test_card(category=test_category) 86 | share_contract = ShareContract.objects.create(user=self.foreign_test_user, category=test_category) 87 | share_contract.decline() 88 | is_shared_with = Card.objects.get(pk=test_card.pk).is_shared_with() 89 | self.assertEqual(list(), is_shared_with) 90 | shared_objects = Card.shared_objects.all(user=self.foreign_test_user) 91 | self.assertEqual(list(), list(shared_objects)) 92 | owned_objects = Card.owned_objects.all(user=self.test_user) 93 | self.assertEqual([test_card], list(owned_objects)) 94 | 95 | def test_shared_card(self): 96 | """Test if a shared card is actually shared 97 | """ 98 | test_category = Category.objects.create(name='Category', description='Description', owner=self.test_user) 99 | test_card = self._create_test_card(category=test_category) 100 | share_contract = ShareContract.objects.create(user=self.foreign_test_user, category=test_category) 101 | share_contract.accept() 102 | is_shared_with = Card.objects.get(pk=test_card.pk).is_shared_with() 103 | self.assertEqual([self.foreign_test_user], is_shared_with) 104 | shared_objects = Card.shared_objects.all(user=self.foreign_test_user) 105 | self.assertEqual([test_card], list(shared_objects)) 106 | -------------------------------------------------------------------------------- /braindump/templates/braindump/braindump_session.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | 3 | {% load static %} 4 | {% load markdown_deux_tags %} 5 | {% load area_rating %} 6 | 7 | {% block title %}{{ card.category }}{% endblock %} 8 | {% block custom_stylesheet_links %}{% endblock %} 9 | {% block custom_javascript_tags %}{% endblock %} 10 | 11 | {% block content %} 12 |
13 |

14 |
15 | Go to category 16 |
17 | {{ card.category.name }} 18 |

19 |
20 |
21 | Card #{{ card.pk }} 22 |
23 | {% area_rating card_placement.area %} 24 |
25 |
26 |
27 | {{ card.question|markdown }} 28 | {% if card.hint %} 29 |
30 |
31 | {{ card.hint|markdown }} 32 |
33 | {% endif %} 34 |
35 |
36 | {{ card.answer|markdown }} 37 |
38 |
39 | 60 |
61 |
62 |
63 |
64 |
65 | 66 | 69 | 77 |
78 |
79 |
80 |
81 |
82 | {% endblock %} 83 | -------------------------------------------------------------------------------- /categories/views/share_contract_gui.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.http import HttpResponseRedirect 4 | from django.shortcuts import get_object_or_404, render, redirect 5 | from django.urls import reverse 6 | from django.utils.safestring import mark_safe 7 | from django.views.generic import ListView, View, DeleteView, FormView 8 | 9 | from categories.exceptions import ShareContractAlreadyRevoked, ShareContractUserIsOwner, \ 10 | ShareContractUserDoesNotExist, ShareContractAlreadyExists 11 | from categories.forms import ShareContractForm 12 | from categories.models import Category, ShareContract 13 | 14 | 15 | class ShareContractBelongsOwnerMixin: 16 | """Mixin that returns all share contracts belonging to the authorized user 17 | """ 18 | def get_queryset(self): 19 | category = get_object_or_404(Category.owned_objects.all(self.request.user), pk=self.kwargs['pk']) 20 | return ShareContract.category_objects.all(category=category).filter(accepted=True) 21 | 22 | 23 | class ShareContractBelongsUserMixin: 24 | """Mixin that returns all share contracts belonging to the authorized user 25 | """ 26 | def get_queryset(self): 27 | return ShareContract.user_objects.all(self.request.user) 28 | 29 | 30 | class CategoryShareContractList(LoginRequiredMixin, ShareContractBelongsOwnerMixin, ListView): 31 | """List all share contracts 32 | """ 33 | paginate_by = 25 34 | paginate_orphans = 5 35 | 36 | def get_context_data(self, **kwargs): 37 | category = get_object_or_404(Category.owned_objects.all(self.request.user), pk=self.kwargs['pk']) 38 | context = super().get_context_data(**kwargs) 39 | context['category'] = category 40 | return context 41 | 42 | 43 | class CategoryShareContractRequest(LoginRequiredMixin, FormView): 44 | """Request a share contract 45 | """ 46 | template_name = 'categories/sharecontract_request_form.html' 47 | form_class = ShareContractForm 48 | 49 | def form_valid(self, form): 50 | category = get_object_or_404(Category.owned_objects.all(self.request.user), pk=self.kwargs['pk']) 51 | try: 52 | form.create_share_contract(category=category, requester_user=self.request.user) 53 | except ShareContractUserIsOwner: 54 | messages.error(self.request, 'You cannot share your category with yourself.') 55 | return self.render_to_response(self.get_context_data(form=form)) 56 | except (ShareContractAlreadyExists, ShareContractUserDoesNotExist): 57 | # Pass these errors for privacy reasons (we do not want to expose user names): 58 | pass 59 | messages.info(self.request, 'The invitation has been sent successfully (assuming that the user exists).') 60 | return super().form_valid(form) 61 | 62 | def get_success_url(self): 63 | return reverse('category-detail', args=(self.kwargs['pk'],)) 64 | 65 | 66 | class CategoryShareContractRevoke(LoginRequiredMixin, ShareContractBelongsOwnerMixin, DeleteView): 67 | """Delete a share contract 68 | """ 69 | pk_url_kwarg = 'share_contract_pk' 70 | context_object_name = 'share_contract' 71 | 72 | def delete(self, request, *args, **kwargs): 73 | share_contract = self.get_object() 74 | success_url = self.get_success_url() 75 | try: 76 | share_contract.revoke() 77 | except ShareContractAlreadyRevoked: 78 | messages.error( 79 | self.request, 80 | mark_safe('Access revocation for {} is already in progress.'.format(share_contract.user)) 81 | ) 82 | else: 83 | messages.success( 84 | self.request, 85 | mark_safe('Access for {} will be revoked.'.format(share_contract.user)) 86 | ) 87 | return HttpResponseRedirect(success_url) 88 | 89 | def get_success_url(self): 90 | return reverse('category-share-contract-list', args=(self.kwargs['pk'],)) 91 | 92 | 93 | class CategoryShareContractAccept(LoginRequiredMixin, ShareContractBelongsUserMixin, View): 94 | """Accept a share contract 95 | """ 96 | http_method_names = ['get', 'post'] 97 | 98 | def get(self, request, share_contract_pk): 99 | share_contract = get_object_or_404(self.get_queryset(), pk=share_contract_pk, accepted=False) 100 | context = { 101 | 'share_contract': share_contract, 102 | } 103 | return render(request, 'categories/sharecontract_confirm_accept.html', context) 104 | 105 | def post(self, request, share_contract_pk): 106 | share_contract = get_object_or_404(self.get_queryset(), pk=share_contract_pk) 107 | if request.POST.get('decision') == 'Accept': 108 | share_contract.accept() 109 | messages.success(request, 'Access granted.') 110 | return redirect(reverse('category-detail', args=(share_contract.category.pk,))) 111 | else: 112 | share_contract.decline() 113 | messages.info(request, 'Invitation declined.') 114 | return redirect(reverse('index')) 115 | -------------------------------------------------------------------------------- /braindump/templates/braindump/braindump_index.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | 3 | {% load static %} 4 | {% load markdown_deux_tags %} 5 | 6 | {% block title %}Braindump{% endblock %} 7 | {% block custom_stylesheet_links %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Braindump

13 |
14 | {% for req in share_contract_requests %} 15 |
16 |
17 |

18 | {{ req.category.name }} 19 |

20 |

21 | 22 | shared by {{ req.category.owner }}, {{ req.category.cards.all|length }} card{{ req.category.cards.all|length|pluralize }}, 23 | {{ req.category.get_mode_display|lower }} mode 24 | 25 |

26 |
27 | {{ req.category.description|markdown }} 28 |
29 |
30 | {% csrf_token %} 31 | 32 | 33 |
34 |
35 |
36 | {% endfor %} 37 | 38 | {% for category in category_list %} 39 |
40 |
41 |

42 | {{ category.name }} 43 |

44 |

45 | 46 | {% if user in category.is_shared_with %} 47 | shared by {{ category.owner }}, 48 | {% elif category.is_shared_with %} 49 | shared, 50 | {% endif %} 51 | {{ category.cards.all|length }} card{{ category.cards.all|length|pluralize }}, 52 | {{ category.get_mode_display|lower }} mode 53 | 54 |

55 |
56 | {{ category.description|markdown }} 57 |
58 |
59 | {% if category.cards.all|length == 0 %} 60 | Create first card 63 | 66 | {% else %} 67 | Start Braindump 69 | 71 | {% endif %} 72 | 82 |
83 |
84 |
85 | {% endfor %} 86 |
87 | 88 | {% if share_contract_requests|length == 0 and category_list|length == 0 %} 89 |
90 |

You have not created any categories yet.

91 |

Create 92 | first category

93 |
94 | {% endif %} 95 | {% endblock %} 96 | -------------------------------------------------------------------------------- /categories/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.urls import reverse 4 | 5 | from categories.exceptions import ShareContractAlreadyRevoked, ShareContractCannotBeAccepted, \ 6 | ShareContractCannotBeDeclined, ShareContractCannotBeRevoked 7 | from categories.signals import share_contract_accepted, share_contract_revoked 8 | 9 | 10 | class CategoryOwnerManager(models.Manager): 11 | def all(self, owner): 12 | """Returns all categories belonging to the owner 13 | """ 14 | return self.filter(owner=owner).all() 15 | 16 | def get(self, owner, *args, **kwargs): 17 | """Returns a category belonging to the owner 18 | """ 19 | return self.filter(owner=owner).get(*args, **kwargs) 20 | 21 | 22 | class CategorySharedManager(models.Manager): 23 | def all(self, user): 24 | """Returns all categories shared with the user 25 | """ 26 | return self.filter(share_contracts__user=user, share_contracts__accepted=True).all() 27 | 28 | def get(self, user, *args, **kwargs): 29 | """Returns a category shared with the user 30 | """ 31 | return self.filter(share_contracts__user=user, share_contracts__accepted=True).get(*args, **kwargs) 32 | 33 | 34 | class Category(models.Model): 35 | MODE_CHOICES = ( 36 | (1, 'Strict'), 37 | (2, 'Defensive'), 38 | ) 39 | name = models.CharField(max_length=128) 40 | description = models.TextField(verbose_name='Description') 41 | mode = models.IntegerField(default=1, choices=MODE_CHOICES) 42 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 43 | objects = models.Manager() 44 | owned_objects = CategoryOwnerManager() 45 | shared_objects = CategorySharedManager() 46 | 47 | class Meta: 48 | ordering = ['name'] 49 | 50 | def __str__(self): 51 | return self.name 52 | 53 | def get_absolute_url(self): 54 | """Get the absolute URL to a single card 55 | """ 56 | return reverse('category-detail', kwargs={'pk': self.pk}) 57 | 58 | def get_cards_for_area(self, area): 59 | """Get all cards of a specific area 60 | """ 61 | return self.cards.filter(area=area) 62 | 63 | def is_shared_with(self): 64 | """Returns a list of users this category is shared with 65 | """ 66 | return [s.user for s in self.share_contracts.filter(accepted=True)] 67 | 68 | 69 | class ShareContractUserManager(models.Manager): 70 | def all(self, user): 71 | """Returns all share contracts belonging to the authorized user 72 | """ 73 | return self.filter(user=user).all() 74 | 75 | def get(self, user, *args, **kwargs): 76 | """Returns a share contract belonging to the authorized user 77 | """ 78 | return self.filter(user=user).get(*args, **kwargs) 79 | 80 | 81 | class ShareContractCategoryManager(models.Manager): 82 | def all(self, category): 83 | """Returns all share contracts belonging to a category 84 | """ 85 | return self.filter(category=category).all() 86 | 87 | def get(self, category, *args, **kwargs): 88 | """Returns a share contract belonging to a category 89 | """ 90 | return self.filter(category=category).get(*args, **kwargs) 91 | 92 | 93 | class ShareContractUserCategoryManager(models.Manager): 94 | def all(self, user, category): 95 | """Returns all share contracts belonging to the authorized user 96 | """ 97 | return self.filter(user=user, category=category).all() 98 | 99 | def get(self, user, category, *args, **kwargs): 100 | """Returns a share contract belonging to the authorized user 101 | """ 102 | return self.filter(user=user, category=category).get(*args, **kwargs) 103 | 104 | 105 | class ShareContract(models.Model): 106 | """ShareContract 107 | 108 | Workflow: 109 | -> Create 110 | self.accepted = False 111 | self.revoked = False 112 | -> Accept 113 | self.accepted = True 114 | self.revoked = False 115 | -> Revoke 116 | self.accepted = True 117 | self.revoked = True 118 | -> Delete 119 | -> Decline 120 | -> Delete 121 | """ 122 | category = models.ForeignKey('categories.Category', on_delete=models.CASCADE, related_name='share_contracts') 123 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 124 | accepted = models.BooleanField(default=False) 125 | revoked = models.BooleanField(default=False) 126 | objects = models.Manager() 127 | user_objects = ShareContractUserManager() 128 | category_objects = ShareContractCategoryManager() 129 | user_category_objects = ShareContractUserCategoryManager() 130 | 131 | class Meta: 132 | unique_together = (('category', 'user'),) 133 | 134 | def accept(self): 135 | """Accept this share contract 136 | """ 137 | if self.accepted or self.revoked: 138 | raise ShareContractCannotBeAccepted() 139 | self.accepted = True 140 | self.save() 141 | share_contract_accepted.send(sender=self.__class__, share_contract=self) 142 | 143 | def decline(self): 144 | """Decline this share contract 145 | """ 146 | if self.accepted or self.revoked: 147 | raise ShareContractCannotBeDeclined() 148 | self.delete() 149 | 150 | def revoke(self): 151 | """Revoke this share contract 152 | """ 153 | if not self.accepted: 154 | raise ShareContractCannotBeRevoked() 155 | if self.revoked: 156 | raise ShareContractAlreadyRevoked() 157 | self.revoked = True 158 | self.save() 159 | share_contract_revoked.send(sender=self.__class__, share_contract=self) 160 | -------------------------------------------------------------------------------- /cards/views/gui.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.http import HttpResponseRedirect 4 | from django.urls import reverse_lazy, reverse 5 | from django.utils.safestring import mark_safe 6 | from django.views.generic.detail import DetailView 7 | from django.views.generic.edit import CreateView, UpdateView, DeleteView 8 | from django.views.generic.list import ListView 9 | 10 | from braindump.models import CardPlacement 11 | from cards.models import Card 12 | from categories.models import Category 13 | 14 | 15 | class CardBelongsOwnerMixin: 16 | """Mixin that returns all cards owned by the authorized user 17 | """ 18 | def get_queryset(self): 19 | return Card.owned_objects.all(self.request.user) 20 | 21 | 22 | class CardBelongsUserMixin: 23 | """Mixin that returns all cards belonging to the authorized user 24 | """ 25 | def get_queryset(self): 26 | owned_card_list = Card.owned_objects.all(self.request.user) 27 | shared_card_list = Card.shared_objects.all(self.request.user) 28 | return owned_card_list | shared_card_list 29 | 30 | 31 | class CardList(LoginRequiredMixin, CardBelongsUserMixin, ListView): 32 | """List all cards 33 | """ 34 | paginate_by = 25 35 | paginate_orphans = 5 36 | template_name = 'cards/card_list.html' 37 | 38 | def get_queryset(self): 39 | return CardPlacement.user_objects.all(self.request.user) 40 | 41 | 42 | class CardDetail(LoginRequiredMixin, CardBelongsUserMixin, DetailView): 43 | """Show detailed information about a card 44 | """ 45 | def get_context_data(self, **kwargs): 46 | context = super(CardDetail, self).get_context_data(**kwargs) 47 | context['card_placement'] = CardPlacement.card_user_objects.get(self.object, self.request.user) 48 | return context 49 | 50 | 51 | class CardCreate(LoginRequiredMixin, CardBelongsUserMixin, CreateView): 52 | """Create a new card 53 | """ 54 | model = Card 55 | fields = ['question', 56 | 'hint', 57 | 'answer', 58 | 'category'] 59 | template_name_suffix = '_create_form' 60 | 61 | def get_form(self, form_class=None): 62 | form = super().get_form(form_class) 63 | form.fields['category'].queryset = Category.owned_objects.all(self.request.user) | \ 64 | Category.shared_objects.all(self.request.user) 65 | return form 66 | 67 | def get_initial(self): 68 | # Pre-select desired category: 69 | return { 70 | 'category': self.request.GET.get('category') 71 | } 72 | 73 | def form_valid(self, form): 74 | if self.request.POST.get('save') == 'Save and create new': 75 | card_object = form.save() 76 | 77 | # Pre-select last category: 78 | query_string = '?category={}'.format(card_object.category.pk) 79 | resp = HttpResponseRedirect(reverse('card-create') + query_string) 80 | 81 | messages.success( 82 | self.request, 83 | mark_safe( 84 | 'Created card in category "{}".'.format( 85 | reverse('card-detail', args=(card_object.pk,)), 86 | reverse('category-detail', args=(card_object.category.pk,)), 87 | card_object.category, 88 | ) 89 | ) 90 | ) 91 | else: 92 | resp = super(CardCreate, self).form_valid(form) 93 | 94 | messages.success( 95 | self.request, 96 | mark_safe( 97 | 'Created card in category "{}". ' 98 | 'Create New'.format( 99 | reverse('card-detail', args=(self.object.pk,)), 100 | reverse('category-detail', args=(self.object.category.pk,)), 101 | self.object.category, 102 | reverse('card-create') + '?category={}'.format(self.object.category.pk), 103 | ) 104 | ) 105 | ) 106 | 107 | return resp 108 | 109 | 110 | class CardUpdate(LoginRequiredMixin, CardBelongsUserMixin, UpdateView): 111 | """Update a card 112 | """ 113 | fields = ['question', 114 | 'hint', 115 | 'answer'] 116 | template_name_suffix = '_update_form' 117 | 118 | def get_form(self, form_class=None): 119 | form = super().get_form(form_class) 120 | return form 121 | 122 | def form_valid(self, form): 123 | if self.request.POST.get('save') == 'Save and Create New': 124 | card_object = form.save() 125 | 126 | # Pre-select last category: 127 | query_string = '?category={}'.format(card_object.category.pk) 128 | resp = HttpResponseRedirect(reverse('card-create') + query_string) 129 | 130 | messages.success( 131 | self.request, 132 | mark_safe( 133 | 'Updated card in category "{}". '.format( 134 | reverse('card-detail', args=(card_object.pk,)), 135 | reverse('category-detail', args=(card_object.category.pk,)), 136 | card_object.category, 137 | ) 138 | ) 139 | ) 140 | else: 141 | resp = super(CardUpdate, self).form_valid(form) 142 | 143 | messages.success( 144 | self.request, 145 | mark_safe( 146 | 'Updated card in category "{}". ' 147 | 'Create New'.format( 148 | reverse('card-detail', args=(self.object.pk,)), 149 | reverse('category-detail', args=(self.object.category.pk,)), 150 | self.object.category, 151 | reverse('card-create') + '?category={}'.format(self.object.category.pk), 152 | ) 153 | ) 154 | ) 155 | 156 | return resp 157 | 158 | 159 | class CardDelete(LoginRequiredMixin, CardBelongsOwnerMixin, DeleteView): 160 | """Delete a card 161 | """ 162 | success_url = reverse_lazy('card-list') 163 | 164 | def delete(self, request, *args, **kwargs): 165 | messages.success(self.request, 'Card deleted.') 166 | return super(CardDelete, self).delete(request, *args, **kwargs) 167 | -------------------------------------------------------------------------------- /categories/templates/categories/category_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "main_authorized.html" %} 2 | {% load static %} 3 | {% load markdown_deux_tags %} 4 | {% load area_rating %} 5 | {% load card_controls %} 6 | {% block title %}{{ category }}{% endblock %} 7 | {% block custom_javascript_tags %} 8 | 9 | {% endblock %} 10 | {% block content %} 11 |

12 | {{ category.name }} 13 |
14 | {% if category.cards.all|length %} 15 | Start Braindump 17 | {% endif %} 18 | Share category 19 | Update 20 | category 21 |
22 |

23 | {% if category.is_shared_with %} 24 |

25 | This category is owned by 26 | {% if category.owner == user %} 27 | you 28 | {% else %} 29 | {{ category.owner }} 30 | {% endif %} 31 | and shared with {{ category.is_shared_with|join:', ' }}. 32 |

33 | {% endif %} 34 |
35 | 36 | {% if category.cards.all|length == 0 %} 37 |
38 |

This category does not contain any cards yet.

39 | Create first card 41 |
42 | {% endif %} 43 | 44 | {% if category.cards.all|length %} 45 |
46 |
47 |
48 |
49 | {{ category.description|markdown }} 50 |
51 |
52 |
53 |
54 |
55 | 56 | 97 |
98 |
99 |
100 | 101 |
102 |

103 | Cards 104 | Create card 107 |

108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | {% for card_placement in card_placements %} 119 | 120 | 127 | 128 | 129 | 130 | {% empty %} 131 | 132 | 133 | 134 | {% endfor %} 135 | 136 |
QuestionArea
121 | {{ card_placement.card.question|truncatechars:128 }} 123 | {% if card_placement.postponed %} 124 | (postponed) 125 | {% endif %} 126 | {% area_rating card_placement.area %}{% card_controls card_placement %}
No cards yet.
137 |
138 |
139 | {% endif %} 140 | 141 |
142 | Delete category 143 |
144 | {% endblock %} 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | memodrop 2 | ======== 3 | 4 | Rapid learning process for people with tight schedules. Implementation of [flash cards](https://en.wikipedia.org/wiki/Flashcard) in Python 3 and Django. 5 | 6 | [![Build Status](https://travis-ci.org/joeig/memodrop.svg?branch=master)](https://travis-ci.org/joeig/memodrop) 7 | 8 | Out of maintenance 9 | ------------------ 10 | 11 | Although I've enjoyed working on this project and sharing it with the community, I decided that v1.2.4 was the final version. It has been a reliable companion on my journey, and - who knows - perhaps it also helped others to get through a demanding stage of life. 12 | 13 | Please mind that dependencies and bugs won't be patched anymore. I'm sure you'll find a lot of great alternatives on GitHub if you search for the term "[leitner method](https://github.com/search?q=leitner+method)". 14 | 15 | Features 16 | -------- 17 | 18 | ### Implementation of the Leitner system 19 | 20 | Improve your learning effectiveness using the [Leitner system](https://en.wikipedia.org/wiki/Leitner_system). It uses a simple algorithm that asks you for cards in the first areas more frequently. Correctly answered cards are moved to the next area. Incorrectly answered cards are moved back to the previous area ("defensive mode") or first area ("strict mode"). 21 | 22 | ### Collaborative categories 23 | 24 | Organize your flash cards in categories and find faster what you're looking for. You can even share your categories with your classmates! 25 | 26 | ### Flash cards with hints 27 | 28 | You don't have any clue what your flash card is talking about? No problem, just write short clues and display them if you need to. 29 | 30 | ### Responsive interface 31 | 32 | Use these features on your mobile phone or tablet as well! 33 | 34 | ### Multi-user 35 | 36 | Create personalized user accounts for your friends with their own categories and cards. 37 | 38 | ### REST API 39 | 40 | If you intend to migrate your existing cards, just use the `/api/v1/categories/` and `/api/v1/cards/` endpoints. 41 | 42 | Screenshots 43 | ----------- 44 | 45 | * [Braindump session page](/docs/screenshots/braindump_session.png?raw=true) 46 | * [Braindump session page (mobile optimized)](/docs/screenshots/braindump_session_mobile.png?raw=true) 47 | * [Index page](/docs/screenshots/braindump_index.png?raw=true) 48 | * [Category detail page](/docs/screenshots/category_detail.png?raw=true) 49 | 50 | Installation 51 | ------------ 52 | 53 | ### Docker setup 54 | 55 | ~~~ bash 56 | docker pull joeig/memodrop:latest 57 | docker run -d -P --name memodrop joeig/memodrop:latest 58 | docker exec -ti memodrop python manage.py createsuperuser 59 | docker port memodrop 60 | ~~~ 61 | 62 | This commands are starting a standalone web service with a local SQlite database in **development mode**. This is the quickest way to get memodrop running. 63 | 64 | In this case, the database file is part of the container volume. If you remove the container, obviously, your data will be removed as well. Also, this setup doesn't work well if you expect more than a few concurrent users. It should never run anywhere else than on localhost. 65 | 66 | #### Production usage 67 | 68 | You should provide a custom settings file in `memodrop/settings/production.py` (template: `production.py.dist`) containing the configuration for an external DBMS like PostgreSQL or MySQL and your own secret key. 69 | 70 | The following guide is using Django's development server again, so consider using Gunicorn, uWSGI or mod_wsgi (WSGI interface: `memodrop.wsgi:application`, `memodrop/wsgi.py`). The static assets should rather be served by a webserver like nginx rather than WSGI (use `python manage.py collectstatic`). 71 | 72 | Enable your custom settings as following: 73 | 74 | ~~~ bash 75 | docker run -d -P --name memodrop \ 76 | -v /path/to/your/production.py:/usr/src/app/memodrop/settings/production.py:ro \ 77 | -e DJANGO_SETTINGS_MODULE=memodrop.settings.production \ 78 | joeig/memodrop:latest 79 | 80 | docker exec -ti memodrop python manage.py createsuperuser 81 | docker port memodrop 82 | ~~~ 83 | 84 | ### Manual setup 85 | 86 | 1. Install Python 3 87 | 2. You may want to create a virtual environment now. 88 | 3. Install the dependencies: `python setup.py install` 89 | 4. Production preparation: Copy `memodrop/settings/production.py.dist` to `memodrop/settings/production.py` and adjust the values 90 | 5. Create a database or let Django do that for you (it will choose SQLite3 by default) 91 | 6. Migrate the database: `python manage.py migrate [--settings memodrop.settings.production]` 92 | 7. Create a super-user account: `python manage.py createsuperuser [--settings memodrop.settings.production]` 93 | 8. * Start the application with its WSGI interface `memodrop/wsgi.py` 94 | * Alternative for developers: Start the standalone web service: `python manage.py runserver [--settings memodrop.settings.production]` 95 | 96 | Create regular user accounts 97 | ---------------------------- 98 | 99 | You can do this with super-user permissions in Django's administration interface (`/admin/`). 100 | 101 | API Authorization 102 | ----------------- 103 | 104 | The API needs a user-specific token to authorize requests. This is accomplished by dispatching an `Authorization: Token ` header with all API requests. 105 | 106 | Tokens are user-specific and can be optained by perfoming a POST request containing the username and password to the authorization endpoint `/api/v1/auth-token/`. Example: 107 | 108 | ~~~ text 109 | $ curl -X POST --data "username=&password=" "http://127.0.0.1:8000/api/v1/auth-token/" 110 | { 111 | "token": "091c4c1204422cb682cc9426d097d492a56a2013" 112 | } 113 | ~~~ 114 | 115 | High-traffic environments 116 | ------------------------- 117 | 118 | ### Enable distributed queue workers 119 | 120 | Basically, every task is executed synchronously. But under certain circumstances, you don't want to block user requests until time-consuming tasks have finished. If you are considering to run a setup with thousands of cards and users, you can enable asynchronous processing using the `Q_CLUSTER` setting in `production.py.dist`. After that, start the cluster workers using the following command: `python manage.py qcluster [--settings memodrop.settings.production]` 121 | 122 | You should also read the official docs regarding [Django Q](https://django-q.readthedocs.io/en/latest/configure.html). 123 | 124 | ### Health check endpoint 125 | 126 | The `/admin/health/` route exposes a status endpoint which usually returns `200 OK` if the application is healthy. 127 | 128 | Contribution 129 | ------------ 130 | 131 | ### Setup 132 | 133 | Setup the development environment: 134 | 135 | ~~~ bash 136 | python setup.py develop 137 | pip install -e ".[dev]" 138 | ~~~ 139 | 140 | There are some fixtures for different scenarios: 141 | 142 | ~~~ bash 143 | python manage.py loaddata demo_users # use demo credentials from categories/fixtures/demo_users.yaml 144 | python manage.py loaddata demo_categories 145 | python manage.py loaddata demo_share_contracts 146 | python manage.py loaddata demo_cards 147 | ~~~ 148 | 149 | ### Test 150 | 151 | Feel free to write and run some unit tests after you've finished your work. 152 | 153 | ~~~ bash 154 | coverage run manage.py test . 155 | coverage report 156 | flake8 157 | ~~~ 158 | 159 | ### Release 160 | 161 | Commit and tag your work (following the [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) guidelines): 162 | 163 | ~~~ bash 164 | bumpversion patch # use major, minor or patch 165 | git push origin master --follow-tags 166 | ~~~ 167 | -------------------------------------------------------------------------------- /categories/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.test import TestCase, Client 4 | from django.urls import reverse 5 | 6 | from braindump.models import CardPlacement 7 | from cards.models import Card 8 | from categories.models import Category, ShareContract 9 | 10 | 11 | class CategoryTestCase(TestCase): 12 | def setUp(self): 13 | """Set up test scenario 14 | """ 15 | self.test_user = User.objects.create_user('test') 16 | self.test_category = Category.objects.create(name='Category 1', description='Description 1', 17 | owner=self.test_user) 18 | self.client = Client() 19 | self.client.force_login(self.test_user) 20 | 21 | self.foreign_test_user = User.objects.create_user('category foreigner') 22 | self.foreign_test_category = Category.objects.create(name='Category Foreign', description='Description Foreign', 23 | owner=self.foreign_test_user) 24 | self.foreign_client = Client() 25 | self.foreign_client.force_login(self.foreign_test_user) 26 | 27 | def _create_test_card(self, suffix='', category=False): 28 | """Create a single test card 29 | """ 30 | if not category: 31 | category = self.test_category 32 | card = Card.objects.create( 33 | question='Question'.format(suffix), 34 | answer='Answer'.format(suffix), 35 | hint='Hint'.format(suffix), 36 | category=category, 37 | ) 38 | return card 39 | 40 | def test_list(self): 41 | """Test if the category list is displayed successfully 42 | """ 43 | url = reverse('category-list') 44 | response = self.client.get(url) 45 | self.assertEqual(response.status_code, 200) 46 | 47 | def test_detail(self): 48 | """Test if the category list is displayed successfully 49 | """ 50 | test_card = self._create_test_card() 51 | url = reverse('category-detail', args=(test_card.category.pk,)) 52 | response = self.client.get(url) 53 | self.assertEqual(response.status_code, 200) 54 | 55 | def test_foreign_category_detail(self): 56 | """Test if the user has no access to foreign categories 57 | """ 58 | test_user = User.objects.create_user('foreigner') 59 | test_category = Category.objects.create( 60 | name='Category 1337', 61 | description='Description 1337', 62 | owner=test_user 63 | ) 64 | url = reverse('category-detail', args=(test_category.pk,)) 65 | 66 | foreign_client = Client() 67 | foreign_client.force_login(test_user) 68 | foreign_response = foreign_client.get(url) 69 | self.assertEqual(foreign_response.status_code, 200) 70 | 71 | response = self.client.get(url) 72 | self.assertEqual(response.status_code, 404) 73 | 74 | def test_delete_empty_category(self): 75 | """Test if the "Delete" button works for an empty category 76 | """ 77 | test_category = Category.objects.create(name='Category 2', description='Description 2', owner=self.test_user) 78 | url = reverse('category-delete', args=(test_category.pk,)) 79 | response = self.client.post(url) 80 | self.assertEqual(response.status_code, 302) 81 | 82 | with self.assertRaises(ObjectDoesNotExist): 83 | Category.objects.get(pk=test_category.pk) 84 | 85 | def test_share_contract_list(self): 86 | """Test if the share contract list is displayed successfully 87 | """ 88 | url = reverse('category-share-contract-list', args=(self.test_category.pk,)) 89 | response = self.client.get(url) 90 | self.assertEqual(response.status_code, 200) 91 | 92 | def test_share_contract_request_valid_user(self): 93 | """Test if a share contract for a valid user works 94 | """ 95 | url = reverse('category-share-contract-request', args=(self.test_category.pk,)) 96 | response = self.client.post(url, data={'username': self.foreign_test_user.username}) 97 | self.assertEqual(response.status_code, 302) 98 | share_contract = ShareContract.user_category_objects.all(user=self.foreign_test_user, 99 | category=self.test_category) 100 | self.assertTrue(share_contract.exists()) 101 | 102 | def test_share_contract_request_owner(self): 103 | """Test if a share contract fails if the target user is equal to the owner of the category 104 | """ 105 | url = reverse('category-share-contract-request', args=(self.test_category.pk,)) 106 | response = self.client.post(url, data={'username': self.test_user.username}) 107 | self.assertEqual(response.status_code, 200) 108 | share_contract = ShareContract.user_category_objects.all(user=self.test_user, 109 | category=self.test_category) 110 | self.assertFalse(share_contract.exists()) 111 | 112 | def test_share_contract_request_invalid_user(self): 113 | """Test if a share contract for a invalid user doesn't work (but should respond exactly like a valid user) 114 | """ 115 | url = reverse('category-share-contract-request', args=(self.test_category.pk,)) 116 | response = self.client.post(url, data={'username': 'cake'}) 117 | self.assertEqual(response.status_code, 302) 118 | share_contract = ShareContract.user_category_objects.all(user=self.foreign_test_user, 119 | category=self.test_category) 120 | self.assertFalse(share_contract.exists()) 121 | 122 | def test_share_contract_request_foreign_category(self): 123 | """Test if a share contract for a invalid category doesn't work 124 | """ 125 | url = reverse('category-share-contract-request', args=(self.foreign_test_category.pk,)) 126 | response = self.client.post(url, data={'username': self.test_user.username}) 127 | self.assertEqual(response.status_code, 404) 128 | share_contract = ShareContract.user_category_objects.all(user=self.foreign_test_user, 129 | category=self.foreign_test_category) 130 | self.assertFalse(share_contract.exists()) 131 | 132 | def test_share_contract_accept(self): 133 | """Test if a share contract can be accepted 134 | """ 135 | test_category = Category.objects.create(name='Category', description='Description', owner=self.test_user) 136 | test_card = self._create_test_card(category=test_category) 137 | share_contract = ShareContract.objects.create(user=self.foreign_test_user, category=test_category) 138 | url = reverse('category-share-contract-accept', args=(share_contract.pk,)) 139 | response = self.foreign_client.post(url, data={'decision': 'Accept'}) 140 | self.assertEqual(response.status_code, 302) 141 | refreshed_share_contract = ShareContract.objects.get(pk=share_contract.pk) 142 | self.assertTrue(refreshed_share_contract.accepted) 143 | test_card_placement = CardPlacement.objects.filter(card=test_card, user=self.foreign_test_user) 144 | self.assertTrue(test_card_placement.exists()) 145 | 146 | def test_share_contract_decline(self): 147 | """Test if a share contract can be declined 148 | """ 149 | share_contract = ShareContract.objects.create(user=self.foreign_test_user, category=self.test_category) 150 | url = reverse('category-share-contract-accept', args=(share_contract.pk,)) 151 | response = self.foreign_client.post(url, data={'decision': 'Decline'}) 152 | self.assertEqual(response.status_code, 302) 153 | refreshed_share_contract = ShareContract.objects.filter(pk=share_contract.pk).all() 154 | self.assertFalse(refreshed_share_contract.exists()) 155 | 156 | def test_share_contract_revoke(self): 157 | """Test if a share contract can be revoked 158 | """ 159 | rand = User.objects.make_random_password(length=32) 160 | test_category = Category.objects.create(name=rand, description='Description', owner=self.test_user) 161 | test_card = self._create_test_card(category=test_category) 162 | share_contract = ShareContract.objects.create(user=self.foreign_test_user, category=test_category) 163 | share_contract.accept() 164 | card_placement = CardPlacement.objects.get(user=self.foreign_test_user, card=test_card) 165 | url = reverse('category-share-contract-revoke', args=(test_category.pk, share_contract.pk,)) 166 | response = self.client.post(url) 167 | self.assertEqual(response.status_code, 302) 168 | refreshed_share_contract = ShareContract.objects.filter(pk=share_contract.pk).all() 169 | self.assertFalse(refreshed_share_contract.exists()) 170 | refreshed_card_placement = CardPlacement.objects.filter( 171 | user=self.foreign_test_user, 172 | card__category__name=rand, 173 | ).first() 174 | self.assertNotEqual(card_placement.card, refreshed_card_placement.card) 175 | -------------------------------------------------------------------------------- /memodrop/static/simplemde-1.11.2/simplemde.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * simplemde v1.11.2 3 | * Copyright Next Step Webs, Inc. 4 | * @link https://github.com/NextStepWebs/simplemde-markdown-editor 5 | * @license MIT 6 | */ 7 | .CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;min-height:300px;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-scroll{min-height:300px}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)} --------------------------------------------------------------------------------