├── openintel ├── static │ └── .keep ├── __init__.py ├── wsgi.py ├── celery.py └── urls.py ├── .python-version ├── openach ├── tests │ ├── __init__.py │ ├── test_util.py │ ├── test_search.py │ ├── test_sitemap.py │ ├── factories.py │ ├── common.py │ ├── test_models.py │ └── test_admin.py ├── migrations │ ├── __init__.py │ ├── 0032_merge_20161020_1841.py │ ├── 0034_merge_20161021_1709.py │ ├── 0012_auto_20160829_2127.py │ ├── 0017_evaluation_timestamp.py │ ├── 0011_board_board_slug.py │ ├── 0010_evidencesource_corroborating.py │ ├── 0044_auto_20180325_0211.py │ ├── 0026_digeststatus_last_attempt.py │ ├── 0043_auto_20180113_0323.py │ ├── 0040_auto_20180111_0250.py │ ├── 0020_auto_20160923_0141.py │ ├── 0024_auto_20160924_1900.py │ ├── 0014_auto_20160908_1915.py │ ├── 0022_auto_20160924_1845.py │ ├── 0045_auto_20180331_2113.py │ ├── 0008_auto_20160826_2300.py │ ├── 0023_auto_20160924_1859.py │ ├── 0027_auto_20160924_2043.py │ ├── 0013_hypothesis_submit_date.py │ ├── 0005_evidencesource_source_date.py │ ├── 0006_evidence_submit_date.py │ ├── 0018_auto_20160919_2318.py │ ├── 0030_auto_20161004_0443.py │ ├── 0031_auto_20161004_1651.py │ ├── 0003_auto_20160825_1622.py │ ├── 0042_auto_20180113_0323.py │ ├── 0016_auto_20160915_1436.py │ ├── 0047_alter_board_board_title.py │ ├── 0032_auto_20161010_1940.py │ ├── 0028_initusersettings.py │ ├── 0025_digeststatus.py │ ├── 0030_auto_20161008_1249.py │ ├── 0038_auto_20180108_0022.py │ ├── 0009_projectnews.py │ ├── 0033_auto_20161020_1844.py │ ├── 0019_boardfollower.py │ ├── 0036_defaultpermissions.py │ ├── 0004_auto_20160826_1651.py │ ├── 0021_usersettings.py │ ├── 0046_auto_20180908_0056.py │ ├── 0015_auto_20160915_1402.py │ ├── 0037_auto_20180107_2344.py │ ├── 0007_auto_20160826_1841.py │ ├── 0039_auto_20180111_0248.py │ └── 0001_initial.py ├── templates │ ├── account │ │ ├── base.html │ │ ├── email │ │ │ ├── email_confirmation_subject.txt │ │ │ └── email_confirmation_message.txt │ │ ├── signup_closed.html │ │ ├── logout.html │ │ ├── signup.html │ │ ├── email_confirm.html │ │ └── login.html │ ├── robots.txt │ ├── boards │ │ ├── email │ │ │ ├── email_digest_subject.txt │ │ │ ├── _notification.txt │ │ │ ├── email_digest_message.txt │ │ │ ├── email_digest_message.html │ │ │ └── _notification.html │ │ ├── _footer.html │ │ ├── notifications │ │ │ ├── _notification.html │ │ │ ├── notifications.html │ │ │ └── _boards.html │ │ ├── _messages.html │ │ ├── boards.html │ │ ├── _meta.html │ │ ├── _board_table.html │ │ ├── create_board.html │ │ ├── add_source.html │ │ ├── add_evidence.html │ │ ├── edit_evidence.html │ │ ├── edit_board.html │ │ ├── _detail_icons.html │ │ ├── user_boards.html │ │ ├── edit_permissions.html │ │ ├── edit_hypothesis.html │ │ ├── add_hypothesis.html │ │ ├── _sharing.html │ │ ├── common │ │ │ └── _pagenav.html │ │ ├── board_audit.html │ │ └── evaluate.html │ ├── teams │ │ ├── view_team.html │ │ ├── edit_team.html │ │ ├── invite.html │ │ ├── teams.html │ │ ├── create_team.html │ │ ├── members.html │ │ ├── _team_table.html │ │ ├── _notifications.html │ │ ├── _details.html │ │ ├── manage_team.html │ │ └── _member_panel.html │ └── contribute.json ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── createadmin.py │ │ ├── setname.py │ │ └── senddigest.py ├── static │ └── boards │ │ └── images │ │ ├── favicon.ico │ │ ├── license.png │ │ ├── fire-extreme.svg │ │ ├── fire-large.svg │ │ ├── fire-mild.svg │ │ └── planet-earth.svg ├── templatetags │ ├── __init__.py │ ├── translation.py │ └── auth_extras.py ├── views │ ├── __init__.py │ ├── util.py │ ├── notifications.py │ └── profiles.py ├── apps.py ├── fixtures │ └── source_tags.yaml ├── signals.py ├── admin.py ├── util.py ├── auth.py ├── sitemap.py ├── donate.py ├── account_adapters.py ├── context_processors.py ├── decorators.py └── tasks.py ├── Procfile ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── encodings.xml ├── GitLink.xml ├── copyright │ ├── profiles_settings.xml │ └── GPLv3.xml ├── modules.xml ├── codeStyleSettings.xml ├── compiler.xml └── codestream.xml ├── .coveragerc ├── conf.py ├── tox.ini ├── .eslintrc.json ├── .gitignore ├── env.sample ├── .github ├── dependabot.yaml └── workflows │ ├── integration.yaml │ └── codeql-analysis.yml ├── contribute.json ├── conftest.py ├── manage.py ├── .pre-commit-config.yaml ├── openintel.iml ├── openach-frontend └── src │ ├── js │ ├── index.js │ ├── board_search.js │ └── notify.js │ └── css │ └── sharing.css ├── package.json ├── webpack.config.prod.js ├── pyproject.toml ├── webpack.config.dev.js ├── webpack.config.base.js ├── SECURITY.md └── README.md /openintel/static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /openach/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openach/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openach/templates/account/base.html: -------------------------------------------------------------------------------- 1 | {% extends "boards/base.html" %} 2 | -------------------------------------------------------------------------------- /openach/__init__.py: -------------------------------------------------------------------------------- 1 | """Analysis of Competing Hypotheses Django application module.""" 2 | -------------------------------------------------------------------------------- /openach/management/__init__.py: -------------------------------------------------------------------------------- 1 | """Analysis of Competing Hypotheses Django application management features.""" 2 | -------------------------------------------------------------------------------- /openach/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | """Analysis of Competing Hypotheses Django application management commands.""" 2 | -------------------------------------------------------------------------------- /openach/static/boards/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twschiller/open-synthesis/HEAD/openach/static/boards/images/favicon.ico -------------------------------------------------------------------------------- /openach/static/boards/images/license.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twschiller/open-synthesis/HEAD/openach/static/boards/images/license.png -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py migrate --noinput 2 | web: gunicorn -c conf.py openintel.wsgi --log-file - 3 | worker: celery worker --app=openintel.celery.app 4 | -------------------------------------------------------------------------------- /openach/templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | {% if disallow_all %} 3 | Disallow: / 4 | {% else %} 5 | Sitemap: {{ sitemap }} 6 | Disallow: /accounts/ 7 | Disallow: /admin/ 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /openach/templates/boards/email/email_digest_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {{site.name}} {{ digest_frequency }} digest for {{ timestamp|date }} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /openach/templates/account/email/email_confirmation_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Please Confirm Your E-mail Address{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = */migrations/*,conf.py,openach/apps.py,openintel/wsgi.py,openintel/settings.py,openintel/celery.py,**/tests.py,node_modules/**,.venv/** 3 | 4 | [run] 5 | plugins = django_coverage_plugin 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /openintel/__init__.py: -------------------------------------------------------------------------------- 1 | """Open Synthesis Django Site.""" 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app # noqa 6 | -------------------------------------------------------------------------------- /openach/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | """Custom template tags for the Analysis of Competing Hypotheses Django application. 2 | 3 | For more information, please see: 4 | https://docs.djangoproject.com/en/1.10/howto/custom-template-tags/ 5 | """ 6 | -------------------------------------------------------------------------------- /.idea/GitLink.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | """Configuration for the gunicorn server. 2 | 3 | For more information, please see: 4 | http://docs.gunicorn.org/en/stable/configure.html#configuration-file 5 | """ 6 | 7 | import gunicorn 8 | 9 | gunicorn.SERVER_SOFTWARE = "gunicorn" 10 | -------------------------------------------------------------------------------- /openach/views/__init__.py: -------------------------------------------------------------------------------- 1 | import openach.views.boards 2 | import openach.views.evidence 3 | import openach.views.hypotheses 4 | import openach.views.notifications 5 | import openach.views.profiles 6 | import openach.views.site 7 | import openach.views.teams 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max-line-length = 119 3 | 4 | [pep257] 5 | # D203 conflicts with D211; see: https://github.com/PyCQA/pydocstyle/issues/141 6 | ignore = D203 7 | 8 | [flake8] 9 | max-line-length=119 10 | exclude=openach/migrations,openach/tests 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 5, 7 | "sourceType": "script", 8 | "ecmaFeatures": { 9 | "jsx": false 10 | } 11 | }, 12 | "rules": { 13 | "semi": ["error", "always"], 14 | "quotes": ["error", "double"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /openach/migrations/0032_merge_20161020_1841.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-20 18:41 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0031_auto_20161004_1651"), 10 | ("openach", "0030_auto_20161008_1249"), 11 | ] 12 | 13 | operations = [] 14 | -------------------------------------------------------------------------------- /openach/migrations/0034_merge_20161021_1709.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-21 17:09 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0032_auto_20161010_1940"), 10 | ("openach", "0033_auto_20161020_1844"), 11 | ] 12 | 13 | operations = [] 14 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /openach/templatetags/translation.py: -------------------------------------------------------------------------------- 1 | """Django Template Localization Helper Methods. 2 | 3 | For more information, please see: 4 | https://docs.djangoproject.com/en/1.10/howto/custom-template-tags/ 5 | """ 6 | 7 | from django.template.defaulttags import register 8 | from django.utils.translation import get_language, to_locale 9 | 10 | 11 | @register.simple_tag() 12 | def get_current_locale(): 13 | """Return the locale for the current language.""" 14 | return to_locale(get_language()) 15 | -------------------------------------------------------------------------------- /openach/migrations/0012_auto_20160829_2127.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-29 21:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0011_board_board_slug"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="board_slug", 16 | field=models.SlugField(max_length=72, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /openach/migrations/0017_evaluation_timestamp.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-19 22:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0016_auto_20160915_1436"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="evaluation", 15 | name="timestamp", 16 | field=models.DateTimeField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /openach/migrations/0011_board_board_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-29 21:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0010_evidencesource_corroborating"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="board", 15 | name="board_slug", 16 | field=models.SlugField(allow_unicode=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /openach/templates/boards/_footer.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 12 | -------------------------------------------------------------------------------- /openach/templates/boards/notifications/_notification.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% if notification.action_object|get_class == 'Hypothesis' %} 4 | {% include 'boards/notifications/_boards.html' %} 5 | {% elif notification.action_object|get_class == 'Evidence' %} 6 | {% include 'boards/notifications/_boards.html' %} 7 | {% elif notification.action_object|get_class == 'Team' or notification.target|get_class == 'Team' %} 8 | {% include 'teams/_notifications.html' %} 9 | {% else %} 10 | {{ notification }} 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /openach/templates/boards/_messages.html: -------------------------------------------------------------------------------- 1 | {% load board_extras %} 2 | {% load i18n %} 3 | 4 | {% if messages %} 5 | {% for message in messages %} 6 | 12 | {% endfor %} 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /openach/migrations/0010_evidencesource_corroborating.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-29 18:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0009_projectnews"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="evidencesource", 15 | name="corroborating", 16 | field=models.BooleanField(default=True), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /openach/templates/teams/view_team.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | {% load notifications_tags %} 5 | {% load bootstrap %} 6 | 7 | {% block title %}{% trans "Team" %} | {{ site.name }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 | {% include 'teams/_details.html' %} 13 |
14 |
15 | {% include 'teams/_member_panel.html' %} 16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /openach/templatetags/auth_extras.py: -------------------------------------------------------------------------------- 1 | """Django Template Authorization Helper Methods. 2 | 3 | For more information, please see: 4 | https://docs.djangoproject.com/en/1.10/howto/custom-template-tags/ 5 | """ 6 | 7 | from django.template.defaulttags import register 8 | 9 | from openach.auth import has_edit_authorization 10 | 11 | 12 | @register.simple_tag 13 | def can_edit(request, board, has_creator=None): 14 | """Return True if the request's user has authorization to edit the resource.""" 15 | return has_edit_authorization(request, board, has_creator=has_creator) 16 | -------------------------------------------------------------------------------- /openach/migrations/0044_auto_20180325_0211.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.11 on 2018-03-25 02:11 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ("openach", "0043_auto_20180113_0323"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name="teamrequest", 17 | unique_together={("team", "inviter", "invitee")}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /openach/migrations/0026_digeststatus_last_attempt.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-24 20:39 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("openach", "0025_digeststatus"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="digeststatus", 16 | name="last_attempt", 17 | field=models.DateTimeField(default=django.utils.timezone.now), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /openach/migrations/0043_auto_20180113_0323.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.6 on 2018-01-13 03:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0042_auto_20180113_0323"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="team", 15 | name="public", 16 | field=models.BooleanField( 17 | default=True, 18 | help_text="Whether or not the team is visible to non-members", 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /openach/templates/teams/edit_team.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load bootstrap %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Edit Team" %} | {{ site.name }}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Edit Team" %}

9 | 10 |
11 | {% csrf_token %} 12 | {{ form|bootstrap }} 13 |
14 |
15 | 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /openach/migrations/0040_auto_20180111_0250.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.6 on 2018-01-11 02:50 2 | 3 | from django.db import migrations 4 | 5 | 6 | def set_hypothesis_creator(apps, schema_editor): 7 | Hypothesis = apps.get_model("openach", "Hypothesis") 8 | for h in Hypothesis.objects.filter(creator__isnull=True).select_related("board"): 9 | h.creator_id = h.board.creator_id 10 | h.save() 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ("openach", "0039_auto_20180111_0248"), 17 | ] 18 | 19 | operations = [ 20 | migrations.RunPython(set_hypothesis_creator), 21 | ] 22 | -------------------------------------------------------------------------------- /openach/templates/teams/invite.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load bootstrap %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Invite Team Members" %} | {{ site.name }}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Invite Team Members" %}

9 | 10 |
11 | {% csrf_token %} 12 | {{ form|bootstrap }} 13 |
14 |
15 | 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /openach/migrations/0020_auto_20160923_0141.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-23 01:41 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("openach", "0019_boardfollower"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="boardfollower", 17 | name="user", 18 | field=models.ForeignKey( 19 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /openach/migrations/0024_auto_20160924_1900.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-24 19:00 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("openach", "0023_auto_20160924_1859"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="usersettings", 17 | name="user", 18 | field=models.OneToOneField( 19 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv 3 | node_modules 4 | *.pyc 5 | db.sqlite3 6 | .env 7 | staticfiles 8 | 9 | # Django artifacts 10 | runserver 11 | 12 | # User-specific stuff: 13 | .idea/workspace.xml 14 | .idea/tasks.xml 15 | .idea/dictionaries 16 | .idea/vcs.xml 17 | .idea/jsLibraryMappings.xml 18 | 19 | # Sensitive or high-churn files: 20 | .idea/dataSources.ids 21 | .idea/dataSources.xml 22 | .idea/dataSources.local.xml 23 | .idea/sqlDataSources.xml 24 | .idea/dynamic.xml 25 | .idea/uiDesigner.xml 26 | .idea/misc.xml 27 | *.log 28 | 29 | 30 | # MacOS files 31 | .DS_Store 32 | 33 | # Testing Artifacts 34 | .coverage 35 | 36 | # NPM/Webpack 37 | webpack-stats.json 38 | openach-frontend/bundles 39 | -------------------------------------------------------------------------------- /openach/migrations/0014_auto_20160908_1915.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-08 19:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0013_hypothesis_submit_date"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="board_desc", 16 | field=models.CharField(max_length=255), 17 | ), 18 | migrations.AlterField( 19 | model_name="evidencesource", 20 | name="source_url", 21 | field=models.URLField(max_length=255), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /openach/migrations/0022_auto_20160924_1845.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-24 18:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0021_usersettings"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="usersettings", 15 | name="digest_frequency", 16 | field=models.PositiveSmallIntegerField( 17 | choices=[(0, "Never"), (1, "Daily"), (2, "Weekly")], 18 | default=1, 19 | verbose_name="email digest frequency", 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /openach/templates/account/email/email_confirmation_message.txt: -------------------------------------------------------------------------------- 1 | {% load account %} 2 | {% user_display user as user_display %} 3 | {% load i18n %} 4 | {% autoescape off %} 5 | 6 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}! 7 | 8 | You're receiving this e-mail because user {{ user_display }} at {{ site_domain }} has given yours as an e-mail address to connect their account. 9 | 10 | To confirm this is correct, go to {{ activate_url }} 11 | {% endblocktrans %}{% endautoescape %} 12 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you from {{ site_name }}! 13 | {{ site_domain }}{% endblocktrans %} 14 | -------------------------------------------------------------------------------- /openach/migrations/0045_auto_20180331_2113.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.11 on 2018-03-31 21:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0044_auto_20180325_0211"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="evidence", 15 | name="event_date", 16 | field=models.DateField( 17 | blank=True, 18 | help_text="The date the event occurred or started", 19 | null=True, 20 | verbose_name="evidence event date", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /openach/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Sign Up Closed" %} | {{ site.name }}{% endblock %} 6 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Sign Up Closed" %}

10 | 11 | {% if invite_request_url %} 12 |

13 | {% trans "Sign up is currently closed to the public." %} 14 | {% trans "Request an invitation." %} 15 |

16 | {% else %} 17 |

{% trans "We are sorry, but the sign up is currently closed." %}

18 | {% endif %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /openach/migrations/0008_auto_20160826_2300.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-26 23:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0007_auto_20160826_1841"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="evidence", 15 | name="event_date", 16 | field=models.DateField(null=True, verbose_name="event date"), 17 | ), 18 | migrations.AlterField( 19 | model_name="evidencesourcetag", 20 | name="tag_name", 21 | field=models.CharField(max_length=64, unique=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /openach/templates/teams/teams.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | {% load static %} 5 | 6 | {% block title %}{% trans "Teams" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block opengraph %} 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |

15 | {% trans "Intelligence Teams" %} 16 | {% trans "Create Team" %} 17 |

18 | 19 | {% include 'teams/_team_table.html' %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /openach/templates/boards/notifications/notifications.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Notifications" %} | {{ site.name }}{% endblock %} 5 | 6 | {% load notifications_tags %} 7 | {% load board_extras %} 8 | {% load static %} 9 | 10 | {% block content %} 11 |

{% trans "Notifications" %}

12 | 13 | 20 | 21 | {% include 'boards/common/_pagenav.html' with paged=notifications %} 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /openach/migrations/0023_auto_20160924_1859.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-24 18:59 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("openach", "0022_auto_20160924_1845"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="usersettings", 17 | name="user", 18 | field=models.ForeignKey( 19 | on_delete=django.db.models.deletion.CASCADE, 20 | to=settings.AUTH_USER_MODEL, 21 | unique=True, 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /openach/migrations/0027_auto_20160924_2043.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-24 20:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0026_digeststatus_last_attempt"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="digeststatus", 15 | name="last_attempt", 16 | field=models.DateTimeField(default=None, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="digeststatus", 20 | name="last_success", 21 | field=models.DateTimeField(default=None, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /openach/apps.py: -------------------------------------------------------------------------------- 1 | """openach Application Configuration. 2 | 3 | For more information, please see: 4 | https://docs.djangoproject.com/en/1.10/ref/applications/ 5 | """ 6 | 7 | from django.apps import AppConfig 8 | 9 | 10 | class OpenACHConfig(AppConfig): 11 | """Django application configuration for the Analysis of Competing Hypotheses (ACH) application. 12 | 13 | For more information, please see: 14 | https://docs.djangoproject.com/en/1.10/ref/applications/ 15 | """ 16 | 17 | name = "openach" 18 | verbose_name = "Open ACH" 19 | default_auto_field = "django.db.models.AutoField" 20 | 21 | def ready(self): 22 | # hook up the signals 23 | import openach.signals # pylint: disable=unused-import 24 | -------------------------------------------------------------------------------- /openach/templates/boards/boards.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | {% load static %} 5 | 6 | {% block title %}{% trans "Boards" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block opengraph %} 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |

15 | {% trans "Intelligence Boards" %} 16 | {% trans "Create Board" %} 17 |

18 | 19 | {% include 'boards/_board_table.html' %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /openach/templates/boards/_meta.html: -------------------------------------------------------------------------------- 1 | {% load translation %} 2 | {% with description=meta_description|default:default_description %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% if twitter_account %} 14 | 15 | {% endif %} 16 | {% endwith %} 17 | -------------------------------------------------------------------------------- /openintel/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for openintel project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.cache.backends.memcached import BaseMemcachedCache 13 | from django.core.wsgi import get_wsgi_application 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openintel.settings") 16 | 17 | application = get_wsgi_application() 18 | 19 | # Fix django closing connection to MemCachier after every request (#11331) 20 | # https://devcenter.heroku.com/articles/django-memcache#optimize-performance 21 | BaseMemcachedCache.close = lambda self, **kwargs: None 22 | -------------------------------------------------------------------------------- /openach/fixtures/source_tags.yaml: -------------------------------------------------------------------------------- 1 | - model: openach.evidencesourcetag 2 | pk: 1 3 | fields: {tag_name: Irrelevant, tag_desc: The claim in the source does not corroborate the evidence} 4 | - model: openach.evidencesourcetag 5 | pk: 2 6 | fields: {tag_name: Retracted, tag_desc: The source retracted the claim} 7 | - model: openach.evidencesourcetag 8 | pk: 3 9 | fields: {tag_name: Secondhand, tag_desc: The source refers to a claim made by another source} 10 | - model: openach.evidencesourcetag 11 | pk: 4 12 | fields: {tag_name: Preliminary, tag_desc: The claim is part of a preliminary and/or unconfirmed report} 13 | - model: openach.evidencesourcetag 14 | pk: 5 15 | fields: {tag_name: Reviewed, tag_desc: The claim in the source has been reviewed by an analyst} 16 | -------------------------------------------------------------------------------- /openach/migrations/0013_hypothesis_submit_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-30 03:05 2 | 3 | import datetime 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("openach", "0012_auto_20160829_2127"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="hypothesis", 16 | name="submit_date", 17 | field=models.DateTimeField( 18 | default=datetime.datetime( 19 | 2016, 8, 30, 3, 5, 15, 430181, tzinfo=datetime.UTC 20 | ), 21 | verbose_name="date added", 22 | ), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /openach/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.dispatch import receiver 3 | 4 | from .models import Board, BoardPermissions, User, UserSettings 5 | 6 | 7 | @receiver(post_save, sender=Board) 8 | def init_board_permissions(sender, **kwargs): 9 | """Link existing benchmark countries to newly created countries.""" 10 | instance = kwargs["instance"] 11 | if kwargs["created"]: 12 | BoardPermissions.objects.create(board=instance) 13 | 14 | 15 | @receiver(post_save, sender=User) 16 | def init_user_settings(sender, **kwargs): 17 | """Link existing benchmark countries to newly created countries.""" 18 | instance = kwargs["instance"] 19 | if kwargs["created"]: 20 | UserSettings.objects.create(user=instance) 21 | -------------------------------------------------------------------------------- /openach/migrations/0005_evidencesource_source_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-26 16:56 2 | 3 | import datetime 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("openach", "0004_auto_20160826_1651"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="evidencesource", 16 | name="source_date", 17 | field=models.DateField( 18 | default=datetime.datetime( 19 | 2016, 8, 26, 16, 56, 39, 255717, tzinfo=datetime.UTC 20 | ), 21 | verbose_name="source date", 22 | ), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /openach/migrations/0006_evidence_submit_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-26 17:29 2 | 3 | import datetime 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("openach", "0005_evidencesource_source_date"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="evidence", 16 | name="submit_date", 17 | field=models.DateTimeField( 18 | default=datetime.datetime( 19 | 2016, 8, 26, 17, 29, 46, 660576, tzinfo=datetime.UTC 20 | ), 21 | verbose_name="date added", 22 | ), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /openach/migrations/0018_auto_20160919_2318.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-19 23:18 2 | 3 | import datetime 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("openach", "0017_evaluation_timestamp"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="evaluation", 16 | name="timestamp", 17 | field=models.DateTimeField( 18 | default=datetime.datetime( 19 | 2016, 9, 19, 23, 18, 19, 119681, tzinfo=datetime.UTC 20 | ), 21 | verbose_name="date evaluated", 22 | ), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /openach/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Sign Out" %} | {{ site.name }}{% endblock %} 6 | {% block head_title %}{% trans "Sign Out" %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Sign Out" %}

10 | 11 |

{% trans 'Are you sure you want to sign out?' %}

12 | 13 |
14 | {% csrf_token %} 15 | {% if redirect_field_value %} 16 | 17 | {% endif %} 18 | 19 |
20 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | SITE_NAME="Open Synthesis (LOCAL)" 2 | SITE_DOMAIN="localhost" 3 | DJANGO_SECRET_KEY="RANDOM_STRING" 4 | DJANGO_LOG_LEVEL="ERROR" 5 | APP_LOG_LEVEL="INFO" 6 | SECURE_SSL_REDIRECT=False 7 | SESSION_COOKIE_SECURE=False 8 | CSRF_COOKIE_SECURE=False 9 | CSRF_COOKIE_HTTPONLY=True 10 | X_FRAME_OPTIONS="DENY" 11 | ROLLBAR_ACCESS_TOKEN= 12 | ROLLBAR_ENABLED=False 13 | SENDGRID_USERNAME= 14 | SENDGRID_PASSWORD= 15 | SLUG_MAX_LENGTH=72 16 | ENABLE_CACHE=True 17 | ADMIN_EMAIL_ADDRESS="admin@localhost" 18 | ACCOUNT_REQUIRED=False 19 | EVIDENCE_REQUIRE_SOURCE=False 20 | TWITTER_ACCOUNT= 21 | DONATE_BITCOIN_ADDRESS= 22 | INVITE_REQUEST_URL= 23 | INVITE_REQUIRED=False 24 | BANNER_MESSAGE= 25 | EDIT_REMOVE_ENABLED=True 26 | PRIVACY_URL= 27 | DIGEST_WEEKLY_DAY=0 28 | REDIS_URL= 29 | CELERY_ALWAYS_EAGER=True 30 | -------------------------------------------------------------------------------- /openach/migrations/0030_auto_20161004_0443.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-04 04:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0029_auto_20161004_0323"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="usersettings", 15 | name="digest_frequency", 16 | field=models.PositiveSmallIntegerField( 17 | choices=[(0, "Never"), (1, "Daily"), (2, "Weekly")], 18 | default=1, 19 | help_text="How frequently to receive email updates containing missed notifications", 20 | verbose_name="email digest frequency", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /openach/migrations/0031_auto_20161004_1651.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-04 16:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0030_auto_20161004_0443"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="usersettings", 15 | name="digest_frequency", 16 | field=models.PositiveSmallIntegerField( 17 | choices=[(0, "Never"), (1, "Daily"), (2, "Weekly")], 18 | default=1, 19 | help_text="How frequently to receive email updates containing new notifications", 20 | verbose_name="email digest frequency", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /openach/templates/boards/email/_notification.txt: -------------------------------------------------------------------------------- 1 | {% load board_extras %}{% if notification.verb == 'added' %}{% if notification.action_object|get_class == 'Hypothesis' %}{{ notification.actor.username }} added hypothesis '{{ notification.action_object }}'{% elif notification.action_object|get_class == 'Evidence' %}{{ notification.actor.username }} added evidence '{{ notification.action_object }}'{% else %}{{ notification }}{% endif %}{% elif notification.verb == 'edited' %}{% if notification.action_object|get_class == 'Hypothesis' %}{{ notification.actor.username }} edited hypothesis '{{ notification.action_object }}'{% elif notification.action_object|get_class == 'Evidence' %}{{ notification.actor.username }} edited evidence '{{ notification.action_object }}'{% else %}{{ notification }}{% endif %}{% else %}{{ notification }}{% endif %} 2 | -------------------------------------------------------------------------------- /openach/migrations/0003_auto_20160825_1622.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-25 16:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0002_auto_20160825_1350"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="evaluation", 15 | name="value", 16 | field=models.PositiveSmallIntegerField( 17 | choices=[ 18 | (0, "N/A"), 19 | (1, "Very Inconsistent"), 20 | (2, "Inconsistent"), 21 | (3, "Neutral"), 22 | (4, "Consistent"), 23 | (5, "Very Consistent"), 24 | ], 25 | default=0, 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /openach/templates/contribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Open Synthesis", 3 | "description": "Open platform for intelligence analysis", 4 | "repository": { 5 | "url": "https://github.com/twschiller/open-synthesis", 6 | "license": "AGPL-3.0", 7 | "tests": "https://travis-ci.org/twschiller/open-synthesis" 8 | }, 9 | "keywords": [ 10 | "python", 11 | "django", 12 | "bootstrap", 13 | "postgresql", 14 | "html5" 15 | ], 16 | "participate": { 17 | "home": "https://github.com/twschiller/open-synthesis/blob/master/CONTRIBUTING.md", 18 | "docs": "https://github.com/twschiller/open-synthesis/wiki" 19 | }, 20 | "bugs": { 21 | "list": "https://github.com/twschiller/open-synthesis/issues", 22 | "report": "https://github.com/twschiller/open-synthesis/issues" 23 | }, 24 | "urls": { 25 | "prod": "https://www.opensynthesis.org" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | enable-beta-ecosystems: true 7 | version: 2 8 | updates: 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | # https://github.com/dependabot/dependabot-core/issues/10478#issuecomment-2691330949 14 | - package-ecosystem: "uv" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | - package-ecosystem: "github-actions" 19 | directory: "/" 20 | schedule: 21 | # Check for updates to GitHub Actions every week 22 | interval: "weekly" 23 | -------------------------------------------------------------------------------- /openach/templates/boards/email/email_digest_message.txt: -------------------------------------------------------------------------------- 1 | {% load account %}{% load i18n %}{% load board_extras %} 2 | {% autoescape off %} 3 | {{ site.name }} {{ digest_frequency }} digest for {{ timestamp|date }} 4 | {% if new_boards %} 5 | New Boards:{% for board in new_boards %} 6 | - {{ board.board_title }}: https://{{ site.domain }}/{{ board|board_url }}{% endfor %} 7 | {% endif %}{% if notifications %}{% for target, for_target in notifications.items %} 8 | Updates for {{ target }}: https://{{ site.domain }}/{{ target|board_url }}{% for notification in for_target %} 9 | - {% include 'boards/email/_notification.txt' %}{% endfor %} 10 | {% endfor %}{% endif %} 11 | You are receiving this e-mail because you're subscribed to receive {{ digest_frequency }} updates from https://{{ site.domain }}/. To modify your email settings, visit your profile at https://{{ site.domain }}/accounts/profile/ 12 | {% endautoescape %} 13 | -------------------------------------------------------------------------------- /openach/templates/teams/create_team.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load bootstrap %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Create Team" %} | {{ site.name }}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Create Team" %}

9 | 10 |

11 | {% blocktrans trimmed %} 12 | To create a team, enter a team name, and optionally provide a description and URL. 13 | You will be able to invite team members once you've created the team. 14 | {% endblocktrans %} 15 |

16 | 17 |
18 | {% csrf_token %} 19 | {{ form|bootstrap }} 20 |
21 |
22 | 23 |
24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /openach/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load bootstrap %} 5 | 6 | {% block title %}{% trans "Sign Up" %} | {{ site.name }}{% endblock %} 7 | {% block head_title %}{% trans "Sign Up" %}{% endblock %} 8 | 9 | {% block content %} 10 |

{% trans "Sign Up" %}

11 | 12 |

{% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

13 | 14 |
15 | {% csrf_token %} 16 | {{ form|bootstrap }} 17 | {% if redirect_field_value %} 18 | 19 | {% endif %} 20 | 21 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /openach/templates/boards/_board_table.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for board in boards %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | 23 |
{% trans "Title" %}{% trans "Posted" %}{% trans "Description" %}{% trans "# Contributors" %}{% trans "# Evaluators" %}
{{ board.board_title }}{{ board.pub_date|timesince }}{{ board.board_desc }}{{ contributors|dict_get:board.id|default_if_none:0 }}{{ evaluators|dict_get:board.id|default_if_none:0 }}
24 | 25 | {% include 'boards/common/_pagenav.html' with paged=boards %} 26 | -------------------------------------------------------------------------------- /openach/templates/boards/create_board.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load bootstrap %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Create Board" %} | {{ site.name }}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Create Board" %}

9 | 10 |

11 | {% blocktrans trimmed %} 12 | To create a board, enter a title, a description, and two initial competing hypotheses. 13 | You will be able to add additional hypotheses once you've created the board. 14 | {% endblocktrans %} 15 |

16 | 17 |
18 | {% csrf_token %} 19 | {{ form|bootstrap }} 20 |
21 |
22 | 23 |
24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /openach/migrations/0042_auto_20180113_0323.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.6 on 2018-01-13 03:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0041_auto_20180113_0301"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="projectnews", 15 | options={"verbose_name_plural": "project news"}, 16 | ), 17 | migrations.AddField( 18 | model_name="team", 19 | name="invitation_required", 20 | field=models.BooleanField(default=True), 21 | ), 22 | migrations.AlterField( 23 | model_name="team", 24 | name="public", 25 | field=models.BooleanField( 26 | default=True, help_text="Whether or not the team is publicly visible" 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /openach/admin.py: -------------------------------------------------------------------------------- 1 | """openach Admin Dashboard Configuration. 2 | 3 | For more information, please see: 4 | https://docs.djangoproject.com/en/1.10/ref/contrib/admin/ 5 | """ 6 | 7 | from django.contrib import admin 8 | 9 | from .models import Board, Evidence, EvidenceSourceTag, Hypothesis, ProjectNews, Team 10 | 11 | 12 | class HypothesisInline(admin.StackedInline): 13 | """Inline editor for an ACH board's hypotheses.""" 14 | 15 | model = Hypothesis 16 | extra = 2 17 | 18 | 19 | class EvidenceInline(admin.StackedInline): 20 | """Inline editor for an ACH board's evidence.""" 21 | 22 | model = Evidence 23 | extra = 2 24 | 25 | 26 | @admin.register(Board) 27 | class BoardAdmin(admin.ModelAdmin): 28 | """Admin interface for editing ACH boards.""" 29 | 30 | inlines = [HypothesisInline, EvidenceInline] 31 | 32 | 33 | admin.site.register(EvidenceSourceTag) 34 | admin.site.register(ProjectNews) 35 | admin.site.register(Team) 36 | -------------------------------------------------------------------------------- /openach/util.py: -------------------------------------------------------------------------------- 1 | """Utility functions for working with iterators and collections.""" 2 | 3 | import itertools 4 | 5 | 6 | def partition(pred, iterable): 7 | """Use a predicate to partition entries into false entries and true entries.""" 8 | # https://stackoverflow.com/questions/8793772/how-to-split-a-sequence-according-to-a-predicate 9 | # NOTE: this might iterate over the collection twice 10 | # NOTE: need to use filter(s) here because we're lazily dealing with iterators 11 | it1, it2 = itertools.tee(iterable) 12 | return ( 13 | itertools.filterfalse(pred, it1), 14 | filter(pred, it2), 15 | ) # pylint: disable=bad-builtin 16 | 17 | 18 | def first_occurrences(iterable): 19 | """Return list where subsequent occurrences of repeat elements have been removed.""" 20 | added = set() 21 | result = [] 22 | for elt in iterable: 23 | if elt not in added: 24 | result.append(elt) 25 | added.add(elt) 26 | return result 27 | -------------------------------------------------------------------------------- /openach/tests/test_util.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from openach import tasks 4 | from openach.util import first_occurrences 5 | 6 | 7 | class UtilMethodTests(TestCase): 8 | def test_first_occurrences_empty(self): 9 | """Test that first_instances() returns an empty list when an empty list is provided.""" 10 | self.assertEqual(first_occurrences([]), []) 11 | 12 | def test_first_occurrences(self): 13 | """Test that first_instances() only preserves the first occurrence in the list.""" 14 | self.assertEqual(first_occurrences(["a", "a"]), ["a"]) 15 | self.assertEqual(first_occurrences(["a", "b", "a"]), ["a", "b"]) 16 | 17 | 18 | class CeleryTestCase(TestCase): 19 | def test_celery_example(self): 20 | """Test that the ``example_task`` task runs with no errors, and returns the correct result.""" 21 | result = tasks.example_task.delay(8, 8) 22 | 23 | self.assertEqual(result.get(), 16) 24 | self.assertTrue(result.successful()) 25 | -------------------------------------------------------------------------------- /.idea/copyright/GPLv3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /contribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Open Synthesis", 3 | "description": "Open platform for CIA-style intelligence analysis", 4 | "repository": { 5 | "url": "https://github.com/twschiller/open-synthesis", 6 | "type": "git", 7 | "license": "AGPL-3.0", 8 | "tests": "https://travis-ci.org/twschiller/open-synthesis", 9 | "clone": "https://github.com/twschiller/open-synthesis.git" 10 | }, 11 | "keywords": [ 12 | "python", 13 | "django", 14 | "bootstrap", 15 | "postgresql", 16 | "html5" 17 | ], 18 | "participate": { 19 | "home": "https://github.com/twschiller/open-synthesis/blob/master/CONTRIBUTING.md", 20 | "docs": "https://github.com/twschiller/open-synthesis/wiki" 21 | }, 22 | "bugs": { 23 | "list": "https://github.com/twschiller/open-synthesis/issues", 24 | "report": "https://github.com/twschiller/open-synthesis/issues" 25 | }, 26 | "urls": { 27 | "prod": "https://www.opensynthesis.org", 28 | "sandbox": "https://open-synthesis-sandbox.herokuapp.com/" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /openach/templates/boards/add_source.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load bootstrap %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Add Source" %} | {{ site.name }}{% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% if corroborating %} 10 |

{% trans "Add Corroborating Source for Evidence" %}

11 | {% else %} 12 |

{% trans "Add Conflicting Source for Evidence" %}

13 | {% endif %} 14 | 15 |
{{ evidence.evidence_desc }}
16 | 17 |
18 | {% csrf_token %} 19 | {{ form|bootstrap }} 20 |
21 |
22 | {% trans "Return to Evidence" %} 23 | 24 |
25 |
26 |
27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /openach/tests/test_search.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | from .common import create_board 5 | 6 | 7 | class BoardSearchTests(TestCase): 8 | def test_can_search_board_by_title(self): 9 | """Test that a client can search for boards by title.""" 10 | create_board("abc", days=-1) 11 | create_board("xyz", days=-1) 12 | response = self.client.get(reverse("openach:board_search") + "?query=ab") 13 | self.assertContains(response, "abc", status_code=200) 14 | self.assertNotContains(response, "xyz", status_code=200) 15 | 16 | def test_can_search_by_board_description(self): 17 | """Test that a client can search for boards by description.""" 18 | board = create_board("abc", days=-1) 19 | board.board_desc = "xyz" 20 | board.save() 21 | create_board("xyz", days=-1) 22 | response = self.client.get(reverse("openach:board_search") + "?query=xy") 23 | self.assertContains(response, "abc", status_code=200) 24 | -------------------------------------------------------------------------------- /openach/migrations/0016_auto_20160915_1436.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-15 14:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0015_auto_20160915_1402"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="evidencesource", 15 | name="removed", 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AlterField( 19 | model_name="board", 20 | name="removed", 21 | field=models.BooleanField(default=False), 22 | ), 23 | migrations.AlterField( 24 | model_name="evidence", 25 | name="removed", 26 | field=models.BooleanField(default=False), 27 | ), 28 | migrations.AlterField( 29 | model_name="hypothesis", 30 | name="removed", 31 | field=models.BooleanField(default=False), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /openach/templates/boards/add_evidence.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load i18n %} 3 | {% load board_extras %} 4 | {% load bootstrap %} 5 | 6 | {% block title %}{% trans "Add Evidence" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block content %} 9 |

10 | {% blocktrans trimmed with board_title=board.board_title %} 11 | Add Evidence for {{ board_title }} 12 | {% endblocktrans %} 13 |

14 | 15 |
{{ board.board_desc }}
16 | 17 |
18 | {% csrf_token %} 19 | {{ evidence_form|bootstrap }} 20 | {{ source_form|bootstrap }} 21 |
22 |
23 | {% trans "Return to Board" %} 24 | 25 |
26 |
27 |
28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /openach/templates/boards/edit_evidence.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load bootstrap %} 4 | {% load i18n %} 5 | 6 | {% block title %}{% trans "Edit Evidence" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Edit Evidence" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|bootstrap }} 14 |
15 |
16 | {% trans "Return to Evidence" %} 17 | {% if allow_remove %} 18 |   19 | {% endif %} 20 | 21 |
22 |
23 |
24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /openach/migrations/0047_alter_board_board_title.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-21 14:55 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("openach", "0046_auto_20180908_0056"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="board", 16 | name="board_title", 17 | field=models.CharField( 18 | db_index=True, 19 | help_text="The board title. Typically phrased as a question asking about what happened in the past, what is happening currently, or what will happen in the future", 20 | max_length=200, 21 | validators=[ 22 | django.core.validators.RegexValidator( 23 | "^((?![!@#^*~`|<>{}+=\\[\\]]).)*$", 24 | "No special characters allowed.", 25 | ) 26 | ], 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /openach/templates/boards/edit_board.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load bootstrap %} 3 | {% load i18n %} 4 | {% load board_extras %} 5 | 6 | {% block title %}{% trans "Edit Board" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Edit Board" %}

10 | 11 |

{% trans "Update the title and/or description for the board." %}

12 | 13 |
14 | {% csrf_token %} 15 | {{ form|bootstrap }} 16 |
17 |
18 | {% trans "Return to Board" %}  19 | {% if allow_remove %} 20 |   21 | {% endif %} 22 | 23 |
24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | # Open Synthesis, an open platform for intelligence analysis 2 | # Copyright (C) 2016-2020 Open Synthesis Contributors. See CONTRIBUTING.md 3 | # file at the top-level directory of this distribution. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | def pytest_configure(config): 20 | import sys 21 | 22 | sys._called_from_test = True 23 | 24 | 25 | def pytest_unconfigure(config): 26 | import sys 27 | 28 | del sys._called_from_test 29 | -------------------------------------------------------------------------------- /openach/auth.py: -------------------------------------------------------------------------------- 1 | """Analysis of Competing Hypotheses Authorization Functions.""" 2 | 3 | from django.core.exceptions import PermissionDenied 4 | 5 | 6 | def has_edit_authorization(request, board, has_creator=None): 7 | """Return True if the user does not have edit rights for the resource. 8 | 9 | :param request: a Django request object 10 | :param board: the Board context 11 | :param has_creator: a model that has a creator member, or None 12 | """ 13 | permissions = board.permissions.for_user(request.user) 14 | return "edit_elements" in permissions or ( 15 | has_creator and request.user.id == has_creator.creator_id 16 | ) 17 | 18 | 19 | def check_edit_authorization(request, board, has_creator=None): 20 | """Raise a PermissionDenied exception if the user does not have edit rights for the resource. 21 | 22 | :param request: a Django request object 23 | :param board: the Board context 24 | :param has_creator: a model that has a creator member, or None 25 | """ 26 | if not has_edit_authorization(request, board, has_creator=has_creator): 27 | raise PermissionDenied() 28 | -------------------------------------------------------------------------------- /openach/templates/boards/_detail_icons.html: -------------------------------------------------------------------------------- 1 | {% load board_extras %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {# The levels here need to match the levels in board_extras. #} 6 | {% if detail_disagreement >= 2.0 %} 7 | 8 | {% trans 10 | 11 | {% elif detail_disagreement >= 1.5 %} 12 | 13 | {% trans 15 | 16 | {% elif detail_disagreement >= 0.5 %} 17 | 18 | {% trans 20 | 21 | {% endif %} 22 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks. 3 | 4 | For more information, please see: 5 | https://docs.djangoproject.com/en/1.10/ref/django-admin/ 6 | """ 7 | import os 8 | import sys 9 | 10 | if __name__ == "__main__": 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openintel.settings") 12 | try: 13 | from django.core.management import execute_from_command_line 14 | except ImportError: # pragma: no cover 15 | # The above import may fail for some other reason. Ensure that the 16 | # issue is really that Django is missing to avoid masking other 17 | # exceptions on Python 2. 18 | try: 19 | # NOTE: the django import is used by 'execute_from_command_line' below 20 | import django # pylint: disable=unused-import 21 | except ImportError: 22 | raise ImportError( 23 | "Couldn't import Django. Are you sure it's installed and " 24 | "available on your PYTHONPATH environment variable? Did you " 25 | "forget to activate a virtual environment?" 26 | ) 27 | raise 28 | execute_from_command_line(sys.argv) 29 | -------------------------------------------------------------------------------- /openach/migrations/0032_auto_20161010_1940.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-10 19:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0031_auto_20161004_1651"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="board_desc", 16 | field=models.CharField( 17 | db_index=True, 18 | help_text="A description providing context around the topic. Helps to clarify which hypotheses and evidence are relevant", 19 | max_length=255, 20 | verbose_name="board description", 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="board", 25 | name="board_title", 26 | field=models.CharField( 27 | db_index=True, 28 | help_text="The board title. Typically phrased as a question asking about what happened in the past, what is happening currently, or what will happen in the future", 29 | max_length=200, 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /openach/templates/boards/user_boards.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | {% load static %} 5 | 6 | {% block title %} 7 | {% blocktrans trimmed with username=user.username %} 8 | {{ username }}'s Boards 9 | {% endblocktrans %} 10 | | {{ site.name }} 11 | {% endblock %} 12 | 13 | {% block opengraph %} 14 | 15 | 16 | {% endblock %} 17 | 18 | {% block content %} 19 |

20 | {% if user == request.user %} 21 | {% blocktrans %} 22 | Boards you have {{ verb }} 23 | {% endblocktrans %} 24 | {% else %} 25 | {% blocktrans trimmed with username=user.username %} 26 | Boards {{ username }} has {{ verb }} 27 | {% endblocktrans %} 28 | {% endif %} 29 | 30 | {% trans "View Account" %} 31 |

32 | 33 | {% include 'boards/_board_table.html' %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /openach/migrations/0028_initusersettings.py: -------------------------------------------------------------------------------- 1 | """Migration to initialize default UserSettings for each user.""" 2 | 3 | from django.db import migrations 4 | 5 | 6 | def forwards_func(apps, schema_editor): 7 | """Create default UserSettings for each user, if they don't already have settings.""" 8 | User = apps.get_model("auth", "User") 9 | UserSettings = apps.get_model("openach", "UserSettings") 10 | db_alias = schema_editor.connection.alias 11 | 12 | # doesn't matter that this is inefficient because we don't have many users yet 13 | for user in User.objects.using(db_alias).all(): 14 | UserSettings.objects.update_or_create(user=user) 15 | 16 | 17 | def reverse_func(apps, schema_editor): 18 | """Remove all UserSettings.""" 19 | UserSettings = apps.get_model("openach", "UserSettings") 20 | db_alias = schema_editor.connection.alias 21 | UserSettings.objects.using(db_alias).all().delete() 22 | 23 | 24 | class Migration(migrations.Migration): 25 | 26 | dependencies = [ 27 | ("openach", "0027_auto_20160924_2043"), 28 | ("auth", "0008_alter_user_username_max_length"), 29 | ] 30 | 31 | operations = [ 32 | migrations.RunPython(forwards_func, reverse_func), 33 | ] 34 | -------------------------------------------------------------------------------- /openach/templates/boards/edit_permissions.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load i18n %} 3 | {% load board_extras %} 4 | {% load bootstrap %} 5 | 6 | {% block title %}{% trans "Modify Permissions" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block content %} 9 |

10 | {% blocktrans trimmed with board_title=board.board_title %} 11 | Modify Permissions for {{ board_title }} 12 | {% endblocktrans %} 13 |

14 | 15 |
16 | {% trans "Warning:" %} 17 | {% blocktrans trimmed %} 18 | Read permissions include access to both new information and historic information. 19 | {% endblocktrans %} 20 |
21 | 22 |
23 | {% csrf_token %} 24 | {{ form|bootstrap }} 25 |
26 |
27 | {% trans "Return to Board" %} 28 | 29 |
30 |
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /openach/templates/boards/edit_hypothesis.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load bootstrap %} 4 | {% load i18n %} 5 | 6 | {% block title %}{% trans "Edit Hypothesis" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block content %} 9 |

10 | {% blocktrans trimmed with board_title=board.board_title %} 11 | Edit Hypothesis for {{ board_title }} 12 | {% endblocktrans %} 13 |

14 | 15 |
{{ board.board_desc }}
16 | 17 |
18 | {% csrf_token %} 19 | {{ form|bootstrap }} 20 |
21 |
22 | {% trans "Return to Board" %}  23 | {% if allow_remove %} 24 |   25 | {% endif %} 26 | 27 |
28 |
29 |
30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /openach/migrations/0025_digeststatus.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-24 19:08 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("openach", "0024_auto_20160924_1900"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="DigestStatus", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("last_success", models.DateTimeField()), 29 | ( 30 | "user", 31 | models.OneToOneField( 32 | on_delete=django.db.models.deletion.CASCADE, 33 | to=settings.AUTH_USER_MODEL, 34 | ), 35 | ), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /openach/sitemap.py: -------------------------------------------------------------------------------- 1 | """Analysis of Competing Hypotheses Django application Sitemap configuration. 2 | 3 | Sitemaps convey meta-information to web search engines/crawlers about the content on the site. For more information, 4 | please see: 5 | https://docs.djangoproject.com/en/1.10/ref/contrib/sitemaps 6 | """ 7 | 8 | from django.contrib.sitemaps import Sitemap 9 | 10 | from .models import Board, Evidence, Hypothesis 11 | 12 | 13 | class BoardSitemap(Sitemap): 14 | """Sitemap containing metadata about ACH boards.""" 15 | 16 | protocol = "https" 17 | changefreq = "daily" 18 | priority = 0.5 19 | 20 | def items(self): 21 | """Return all the items for the Sitemap.""" 22 | return Board.objects.public() 23 | 24 | def lastmod(self, obj): # pylint: disable=no-self-use 25 | """Return the last time the board or its content was structurally modified.""" 26 | 27 | # NOTE: self parameter is required to match the Sitemap interface 28 | def _last_obj(class_): 29 | return max( 30 | (o.submit_date for o in class_.objects.filter(board=obj)), 31 | default=obj.pub_date, 32 | ) 33 | 34 | return max([obj.pub_date, _last_obj(Evidence), _last_obj(Hypothesis)]) 35 | -------------------------------------------------------------------------------- /openach/templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | {% load i18n %} 3 | {% load account %} 4 | 5 | {% block title %}{% trans "Confirm E-mail Address" %} | {{ site.name }}{% endblock %} 6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Confirm E-mail Address" %}

10 | 11 | {% if confirmation %} 12 | 13 | {% user_display confirmation.email_address.user as user_display %} 14 | 15 |

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

16 | 17 |
18 | {% csrf_token %} 19 | 20 |
21 | 22 | {% else %} 23 | 24 | {% url 'account_email' as email_url %} 25 | 26 |

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

27 | 28 | {% endif %} 29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /openach/templates/boards/add_hypothesis.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load bootstrap %} 4 | {% load i18n %} 5 | 6 | {% block title %}{% trans "Add Hypothesis" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block content %} 9 |

10 | {% blocktrans trimmed with board_title=board.board_title %} 11 | Add Hypothesis for {{ board_title }} 12 | {% endblocktrans %} 13 |

14 | 15 |
{{ board.board_desc }}
16 | 17 |

{% trans "Existing Hypotheses" %}

18 | 23 | 24 |

{% trans "New Hypothesis" %}

25 |
26 | {% csrf_token %} 27 | {{ form|bootstrap }} 28 |
29 |
30 | {% trans "Return to Board" %} 31 | 32 |
33 |
34 |
35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /openach/donate.py: -------------------------------------------------------------------------------- 1 | """Analysis of Competing Hypotheses donation utility methods.""" 2 | 3 | from io import BytesIO 4 | 5 | import qrcode 6 | from django.utils.http import urlencode 7 | from qrcode.image.svg import SvgPathImage 8 | 9 | 10 | def bitcoin_donation_url(site_name, address): 11 | """Return a Bitcoin donation URL for DONATE_BITCOIN_ADDRESS or None.""" 12 | if address: 13 | msg = f"Donate to {site_name}" 14 | url = "bitcoin:{}?{}".format(address, urlencode({"message": msg})) 15 | return url 16 | else: 17 | return None 18 | 19 | 20 | def make_qr_code( 21 | message, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=4 22 | ): 23 | """Return an in-memory SVG QR code containing the given message.""" 24 | # https://pypi.python.org/pypi/qrcode/5.3 25 | # qrcode.constants.ERROR_CORRECT_M means about 15% or less errors can be corrected. 26 | code = qrcode.QRCode( 27 | version=1, 28 | error_correction=error_correction, 29 | box_size=box_size, 30 | border=border, 31 | ) 32 | code.add_data(message) 33 | code.make(fit=True) 34 | img = code.make_image(image_factory=SvgPathImage) 35 | raw = BytesIO() 36 | img.save(raw) 37 | raw.flush() 38 | return raw 39 | -------------------------------------------------------------------------------- /openach/templates/boards/_sharing.html: -------------------------------------------------------------------------------- 1 | {% load board_extras %} 2 | {% load i18n %} 3 | 4 | {% with encoded_title=title|urlencode:"" encoded_url=url|urlencode:"" %} 5 |
6 | 9 | 11 | 13 | 15 | 17 | 19 |
20 | {% endwith %} 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/asottile/seed-isort-config 9 | rev: v2.2.0 10 | hooks: 11 | - id: seed-isort-config 12 | - repo: https://github.com/asottile/pyupgrade 13 | rev: v3.19.1 14 | hooks: 15 | - id: pyupgrade 16 | args: ["--py313-plus"] 17 | - repo: https://github.com/adamchainz/django-upgrade 18 | rev: 1.23.1 # replace with latest tag on GitHub 19 | hooks: 20 | - id: django-upgrade 21 | args: [--target-version, "4.2"] # Replace with Django version 22 | - repo: https://github.com/pycqa/isort 23 | rev: 6.0.1 24 | hooks: 25 | - id: isort 26 | - repo: https://github.com/psf/black 27 | rev: 25.1.0 28 | hooks: 29 | - id: black 30 | - repo: https://github.com/pre-commit/mirrors-prettier 31 | rev: v3.1.0 32 | hooks: 33 | - id: prettier 34 | files: \.(js|scss)$ 35 | exclude_types: [html] 36 | additional_dependencies: 37 | - prettier@3.5.3 # Required because pre-commit is poorly maintained 38 | - repo: https://github.com/rhysd/actionlint 39 | rev: v1.7.7 40 | hooks: 41 | - id: actionlint 42 | -------------------------------------------------------------------------------- /openach/migrations/0030_auto_20161008_1249.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-08 12:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0029_auto_20161004_0323"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="evidencesource", 15 | name="source_url_description", 16 | field=models.CharField( 17 | default="", max_length=1000, verbose_name="source url description" 18 | ), 19 | ), 20 | migrations.AddField( 21 | model_name="evidencesource", 22 | name="source_url_title", 23 | field=models.CharField( 24 | default="", max_length=255, verbose_name="source url title" 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="usersettings", 29 | name="digest_frequency", 30 | field=models.PositiveSmallIntegerField( 31 | choices=[(0, "Never"), (1, "Daily"), (2, "Weekly")], 32 | default=1, 33 | help_text="How frequently to receive email updates containing missed notifications", 34 | verbose_name="email digest frequency", 35 | ), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /openach/templates/teams/members.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | {% load static %} 5 | 6 | {% block title %}{% trans "Team Members" %} | {{ site.name }}{% endblock %} 7 | 8 | {% block content %} 9 |

10 | {% blocktrans with team_name=team.name %}Team Members for {{ team_name }}{% endblocktrans %} 11 |

12 | 28 | {% include 'boards/common/_pagenav.html' with paged=members %} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /openach/static/boards/images/fire-extreme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /openach/static/boards/images/fire-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /openach/static/boards/images/fire-mild.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /openach/migrations/0038_auto_20180108_0022.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.6 on 2018-01-08 00:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0037_auto_20180107_2344"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="boardpermissions", 15 | name="read_board", 16 | field=models.PositiveSmallIntegerField( 17 | choices=[ 18 | (0, "Only Me"), 19 | (1, "Collaborators"), 20 | (2, "Registered Users"), 21 | (3, "Public"), 22 | ], 23 | default=3, 24 | help_text="Who can view and evaluate the board?", 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="boardpermissions", 29 | name="read_comments", 30 | field=models.PositiveSmallIntegerField( 31 | choices=[ 32 | (0, "Only Me"), 33 | (1, "Collaborators"), 34 | (2, "Registered Users"), 35 | (3, "Public"), 36 | ], 37 | default=3, 38 | help_text="Who can view board comments?", 39 | ), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /openach/migrations/0009_projectnews.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-28 22:41 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("openach", "0008_auto_20160826_2300"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="ProjectNews", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("content", models.CharField(max_length=1024)), 29 | ("pub_date", models.DateTimeField(verbose_name="date published")), 30 | ( 31 | "author", 32 | models.ForeignKey( 33 | null=True, 34 | on_delete=django.db.models.deletion.CASCADE, 35 | to=settings.AUTH_USER_MODEL, 36 | ), 37 | ), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /openintel.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /openach/management/commands/createadmin.py: -------------------------------------------------------------------------------- 1 | """Django admin command to create an admin account based on the project settings.""" 2 | 3 | from django.conf import settings 4 | from django.contrib.auth.models import User 5 | from django.core.management.base import BaseCommand, CommandError 6 | 7 | 8 | class Command(BaseCommand): 9 | """Django admin command to create an admin account based on the project settings variables. 10 | 11 | Requires the following settings: ADMIN_USERNAME, ADMIN_PASSWORD, ADMIN_EMAIL_ADDRESS. 12 | """ 13 | 14 | help = "Automatically create superuser based on environment variables." 15 | 16 | def handle(self, *args, **options): 17 | """Handle the command invocation.""" 18 | email = getattr(settings, "ADMIN_EMAIL_ADDRESS", None) 19 | username = getattr(settings, "ADMIN_USERNAME", None) 20 | password = getattr(settings, "ADMIN_PASSWORD", None) 21 | 22 | if not email or not username or not password: 23 | raise CommandError( 24 | "ADMIN_USERNAME, ADMIN_PASSWORD, and ADMIN_EMAIL_ADDRESS must be set" 25 | ) 26 | 27 | admin = User(username=username, email=email) 28 | admin.set_password(password) 29 | admin.is_superuser = True 30 | admin.is_staff = True 31 | admin.save() 32 | 33 | msg = f"Successfully configured admin {username} ({email})" 34 | self.stdout.write(self.style.SUCCESS(msg)) # pylint: disable=no-member 35 | -------------------------------------------------------------------------------- /openach-frontend/src/js/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Open Synthesis, an open platform for intelligence analysis 3 | * Copyright (C) 2016 Open Synthesis Contributors. See CONTRIBUTING.md 4 | * file at the top-level directory of this distribution. 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | import "bootstrap"; 20 | import "bootstrap-datepicker"; 21 | import "@selectize/selectize"; 22 | 23 | import "src/board_search"; 24 | import "src/notify"; 25 | 26 | import "bootstrap/dist/css/bootstrap.min.css"; 27 | import "bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css"; 28 | import "@selectize/selectize/dist/css/selectize.css"; 29 | import "@selectize/selectize/dist/css/selectize.bootstrap3.css"; 30 | import "css/sharing.css"; 31 | import "css/boards.css"; 32 | 33 | $("form[name='switchLanguageForm']").change(function () { 34 | $(this).submit(); 35 | }); 36 | 37 | $(".selectize").selectize(); 38 | -------------------------------------------------------------------------------- /openach/migrations/0033_auto_20161020_1844.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-20 18:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0032_merge_20161020_1841"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="evidencesource", 15 | name="source_url_description", 16 | ), 17 | migrations.RemoveField( 18 | model_name="evidencesource", 19 | name="source_url_title", 20 | ), 21 | migrations.AddField( 22 | model_name="evidencesource", 23 | name="source_description", 24 | field=models.CharField( 25 | default="", max_length=1000, verbose_name="source description" 26 | ), 27 | ), 28 | migrations.AddField( 29 | model_name="evidencesource", 30 | name="source_title", 31 | field=models.CharField( 32 | default="", max_length=255, verbose_name="source title" 33 | ), 34 | ), 35 | migrations.AlterField( 36 | model_name="usersettings", 37 | name="digest_frequency", 38 | field=models.PositiveSmallIntegerField( 39 | choices=[(0, "Never"), (1, "Daily"), (2, "Weekly")], 40 | default=1, 41 | help_text="How frequently to receive email updates containing new notifications", 42 | verbose_name="email digest frequency", 43 | ), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /openach/migrations/0019_boardfollower.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-23 01:38 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("openach", "0018_auto_20160919_2318"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="BoardFollower", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("is_creator", models.BooleanField(default=False)), 27 | ("is_contributor", models.BooleanField(default=False)), 28 | ("is_evaluator", models.BooleanField(default=False)), 29 | ("update_timestamp", models.DateTimeField()), 30 | ( 31 | "board", 32 | models.ForeignKey( 33 | on_delete=django.db.models.deletion.CASCADE, 34 | related_name="followers", 35 | to="openach.Board", 36 | ), 37 | ), 38 | ( 39 | "user", 40 | models.ForeignKey( 41 | on_delete=django.db.models.deletion.CASCADE, to="openach.Board" 42 | ), 43 | ), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /.idea/codestream.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /openach/account_adapters.py: -------------------------------------------------------------------------------- 1 | """Custom account adapters.""" 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | from invitations.app_settings import app_settings 5 | 6 | from .models import UserSettings 7 | 8 | 9 | class InvitationsAdapter(DefaultAccountAdapter): 10 | """Django invitations adapter. 11 | 12 | Taken from https://github.com/bee-keeper/django-invitations/blob/master/invitations/models.py 13 | """ 14 | 15 | # django-invitations does some hackery when using allauth in their models module. 16 | 17 | def is_open_for_signup(self, request): 18 | """Return True if site is not invitation only, or if the user accessed the signup from an invitation.""" 19 | if hasattr(request, "session") and request.session.get( 20 | "account_verified_email" 21 | ): 22 | return True 23 | elif app_settings.INVITATION_ONLY is True: 24 | # site is ONLY open for invites 25 | return False 26 | else: 27 | # site is open to signup 28 | return True 29 | 30 | 31 | class AccountAdapter(InvitationsAdapter): 32 | """Account adapter to handle account actions, e.g., sign-up. 33 | 34 | For more information, see: 35 | https://django-allauth.readthedocs.io/en/latest/advanced.html#creating-and-populating-user-instances 36 | """ 37 | 38 | def save_user(self, request, user, form, commit=True): 39 | """Initialize default settings for user when on signup.""" 40 | saved = super().save_user(request, user, form, commit) 41 | # user settings is created using signal 42 | # UserSettings.objects.create(user=saved) 43 | return saved 44 | -------------------------------------------------------------------------------- /openach/templates/boards/email/email_digest_message.html: -------------------------------------------------------------------------------- 1 | {% load account %} 2 | {% load i18n %} 3 | {% load board_extras %} 4 | 5 |

6 | {% blocktrans trimmed with site.name as site_name and timestamp|date as date_text %} 7 | {{ site_name }} {{ digest_frequency }} digest for {{ date_text }} 8 | {% endblocktrans %} 9 |

10 | 11 | {% if new_boards %} 12 |

{% trans "New Boards" %}

13 | 18 | {% endif %} 19 | 20 | {% if notifications %} 21 |

{% trans "Board Updates" %}

22 | 23 | {% for target, for_target in notifications.items %} 24 |

25 | {% blocktrans trimmed with site.domain as site_domain and target|board_url as custom_url and target.board_title as board_title %} 26 | Board {{ board_title }} 27 | {% endblocktrans %} 28 |

29 | 34 | {% endfor %} 35 | {% endif %} 36 | 37 |

38 | {% blocktrans trimmed with site_domain=site.domain site_name=site.name %} 39 | You are receiving this e-mail because you're subscribed to receive {{ digest_frequency }} updates 40 | from {{ site_name }}. To modify your email settings, visit 41 | your account profile. 42 | {% endblocktrans %} 43 |

44 | -------------------------------------------------------------------------------- /openach/migrations/0036_defaultpermissions.py: -------------------------------------------------------------------------------- 1 | """Initialize default public board permissions.""" 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | from openach.models import AuthLevels 7 | 8 | 9 | def forwards_func(apps, schema_editor): 10 | """Create default permissions for each board, if they don't already have permissions.""" 11 | BoardPermissions = apps.get_model("openach", "BoardPermissions") 12 | Board = apps.get_model("openach", "Board") 13 | db_alias = schema_editor.connection.alias 14 | 15 | default_read = ( 16 | AuthLevels.registered.key 17 | if getattr(settings, "ACCOUNT_REQUIRED", True) 18 | else AuthLevels.anyone.key 19 | ) 20 | 21 | for board in Board.objects.using(db_alias).all(): 22 | if not BoardPermissions.objects.filter(board=board).exists(): 23 | BoardPermissions.objects.create( 24 | board=board, 25 | read_board=default_read, 26 | read_comments=default_read, 27 | add_comments=AuthLevels.collaborators.key, 28 | add_elements=AuthLevels.collaborators.key, 29 | edit_elements=AuthLevels.collaborators.key, 30 | ) 31 | 32 | 33 | def reverse_func(apps, schema_editor): 34 | """Do nothing.""" 35 | # we could remove board permissions that currently have default values, but there's no compelling reason to do this 36 | # at this point. 37 | pass 38 | 39 | 40 | class Migration(migrations.Migration): 41 | 42 | dependencies = [ 43 | ("openach", "0035_boardpermissions"), 44 | ] 45 | 46 | operations = [ 47 | migrations.RunPython(forwards_func, reverse_func), 48 | ] 49 | -------------------------------------------------------------------------------- /openach/context_processors.py: -------------------------------------------------------------------------------- 1 | """Django template context processors.""" 2 | 3 | from django.conf import settings 4 | from django.contrib.sites.shortcuts import get_current_site 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | 8 | def site(request): 9 | """Return a template context with the current site as 'site'.""" 10 | # NOTE: get_current_site caches the result from the db 11 | # See: https://docs.djangoproject.com/en/1.10/ref/contrib/sites/#caching-the-current-site-object 12 | return {"site": get_current_site(request)} 13 | 14 | 15 | def meta(request): 16 | """Return a template context with site's social account information.""" 17 | site_name = get_current_site(request) 18 | return { 19 | "twitter_account": getattr(settings, "TWITTER_ACCOUNT", None), 20 | "facebook_account": getattr(settings, "FACEBOOK_ACCOUNT", None), 21 | "default_description": _( 22 | "{name} is an open platform for CIA-style intelligence analysis" 23 | ).format( 24 | name=site_name 25 | ), # nopep8 26 | "default_keywords": [ 27 | _("Analysis of Competing Hypotheses"), 28 | _("ACH"), 29 | _("intelligence analysis"), 30 | _("current events"), 31 | ], 32 | } 33 | 34 | 35 | def invite(dummy_request): 36 | """Return a template context with the site's invitation configuration.""" 37 | return { 38 | "invite_request_url": getattr(settings, "INVITE_REQUEST_URL", None), 39 | } 40 | 41 | 42 | def banner(dummy_request): 43 | """Return a template context with a the site's banner configuration.""" 44 | return { 45 | "banner": getattr(settings, "BANNER_MESSAGE", None), 46 | } 47 | -------------------------------------------------------------------------------- /openach/migrations/0004_auto_20160826_1651.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-26 16:51 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("openach", "0003_auto_20160825_1622"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="EvidenceSource", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("source_url", models.URLField()), 29 | ("submit_date", models.DateTimeField(verbose_name="date added")), 30 | ], 31 | ), 32 | migrations.RemoveField( 33 | model_name="evidence", 34 | name="evidence_url", 35 | ), 36 | migrations.AddField( 37 | model_name="evidencesource", 38 | name="evidence", 39 | field=models.ForeignKey( 40 | on_delete=django.db.models.deletion.CASCADE, to="openach.Evidence" 41 | ), 42 | ), 43 | migrations.AddField( 44 | model_name="evidencesource", 45 | name="uploader", 46 | field=models.ForeignKey( 47 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 48 | ), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-synthesis", 3 | "version": "0.0.3", 4 | "description": "Open platform for CIA-style intelligence analysis", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "npx webpack --config webpack.config.prod.js", 9 | "watch": "NODE_ENV='development' npx webpack --config webpack.config.dev.js --watch", 10 | "postinstall": "npm run build", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/twschiller/open-synthesis.git" 16 | }, 17 | "keywords": [ 18 | "intelligence analysis", 19 | "ACH", 20 | "analysis of competing hypotheses" 21 | ], 22 | "author": "Todd Schiller", 23 | "license": "AGPL-3.0", 24 | "bugs": { 25 | "url": "https://github.com/twschiller/open-synthesis/issues" 26 | }, 27 | "homepage": "https://github.com/twschiller/open-synthesis#readme", 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "@babel/core": "^7.28.5", 31 | "@babel/preset-env": "^7.28.5", 32 | "babel-loader": "^10.0.0", 33 | "bootstrap": "^3.4.1", 34 | "bootstrap-datepicker": "^1.10.1", 35 | "css-loader": "^7.1.2", 36 | "eslint": "^9.39.1", 37 | "file-loader": "^6.2.0", 38 | "jquery": "^3.7.1", 39 | "mini-css-extract-plugin": "^2.9.4", 40 | "css-minimizer-webpack-plugin": "^7.0.4", 41 | "@selectize/selectize": "^0.15.2", 42 | "style-loader": "^4.0.0", 43 | "webpack": "^5.103.0", 44 | "webpack-bundle-tracker": "^1.8.1", 45 | "webpack-cli": "^6.0.1", 46 | "webpack-merge": "^6.0.1" 47 | }, 48 | "engine-strict": true, 49 | "engines": { 50 | "node": ">=18.13 <19", 51 | "npm": "8.x" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /openach/templates/boards/common/_pagenav.html: -------------------------------------------------------------------------------- 1 | {% load board_extras %} 2 | {% load i18n %} 3 | 4 | 41 | -------------------------------------------------------------------------------- /openach-frontend/src/js/board_search.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Open Synthesis, an open platform for intelligence analysis 3 | * Copyright (C) 2016 Open Synthesis Contributors. See CONTRIBUTING.md 4 | * file at the top-level directory of this distribution. 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import "@selectize/selectize"; 21 | 22 | $(document).ready(function () { 23 | $("#board-search").selectize({ 24 | valueField: "url", 25 | labelField: "board_title", 26 | searchField: ["board_title", "board_desc"], 27 | maxItems: 1, 28 | create: false, 29 | render: { 30 | option: function (item) { 31 | return "
" + item.board_title + "
"; 32 | }, 33 | }, 34 | load: function (query, callback) { 35 | if (!query.length) return callback(); 36 | $.ajax({ 37 | url: "/api/boards/?query=" + query, 38 | type: "GET", 39 | error: function () { 40 | callback(); 41 | }, 42 | success: function (data) { 43 | callback(data); 44 | }, 45 | }); 46 | }, 47 | onItemAdd: function (value) { 48 | window.location.href = value; 49 | }, 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /openach/templates/teams/_team_table.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load board_extras %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for team in teams %} 15 | 16 | 34 | 35 | 36 | 37 | 38 | {% endfor %} 39 | 40 |
{% trans "Team" %}{% trans "Created" %}{% trans "Members" %}{% trans "Description" %}
17 | {{ team.name }} 18 | {% contains_tag user_teams team.id as is_member %} 19 | {% contains_tag user_pending team.id as is_pending %} 20 | {% contains_tag user_invites team.id as has_invite %} 21 | {% contains_tag user_owns team.id as is_owner %} 22 | {% if is_owner %} 23 |  {% trans "Owner" %} 24 | {% elif is_member %} 25 |  {% trans "Member" %} 26 | {% elif has_invite %} 27 |  {% trans "Invited" %} 28 | {% elif is_pending %} 29 |   {% trans "Membership Pending" %} 30 | {% elif team.invitation_required %} 31 |  {% trans "Invite-Only" %} 32 | {% endif %} 33 | {{ team.create_timestamp|timesince }}{{ team.members.count }}{{ team.description }}
41 | 42 | {% include 'boards/common/_pagenav.html' with paged=teams %} 43 | -------------------------------------------------------------------------------- /openach/management/commands/setname.py: -------------------------------------------------------------------------------- 1 | """Django admin command to set the site from the project settings. 2 | 3 | For more information, please see: 4 | https://docs.djangoproject.com/en/1.10/ref/contrib/sites/ 5 | """ 6 | 7 | from django.conf import settings 8 | from django.contrib.sites.models import Site 9 | from django.core.management.base import BaseCommand, CommandError 10 | 11 | 12 | class Command(BaseCommand): 13 | """Django admin command to set the site from the project settings. 14 | 15 | Requires the following settings: SITE_ID, SITE_NAME, and SITE_DOMAIN. 16 | """ 17 | 18 | help = "Sets the site name and domain from the environment" 19 | 20 | def handle(self, *args, **options): 21 | """Handle the command invocation.""" 22 | site_id = getattr(settings, "SITE_ID", None) 23 | site_name = getattr(settings, "SITE_NAME", None) 24 | site_domain = getattr(settings, "SITE_DOMAIN", None) 25 | 26 | if not site_id: 27 | raise CommandError("No SITE_ID specified in project settings") 28 | if not site_name: 29 | raise CommandError("No SITE_NAME specified in project settings") 30 | if not site_domain: 31 | raise CommandError("No SITE_DOMAIN specified in project settings") 32 | 33 | try: 34 | site = Site.objects.get(pk=site_id) 35 | except Site.DoesNotExist: # pylint: disable=no-member 36 | raise CommandError('Site "%s" does not exist' % site_id) 37 | 38 | site.name = site_name 39 | site.domain = site_domain 40 | site.save() 41 | 42 | msg = 'Successfully configured site #{}; name: "{}"; domain: {}'.format( 43 | site_id, 44 | site_name, 45 | site_domain, 46 | ) 47 | self.stdout.write(self.style.SUCCESS(msg)) # pylint: disable=no-member 48 | -------------------------------------------------------------------------------- /openach/migrations/0021_usersettings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-24 18:30 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | import openach.models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ("openach", "0020_auto_20160923_0141"), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name="UserSettings", 20 | fields=[ 21 | ( 22 | "id", 23 | models.AutoField( 24 | auto_created=True, 25 | primary_key=True, 26 | serialize=False, 27 | verbose_name="ID", 28 | ), 29 | ), 30 | ( 31 | "digest_frequency", 32 | models.PositiveSmallIntegerField( 33 | choices=[ 34 | (openach.models.DigestFrequency.never.key, "Never"), 35 | (openach.models.DigestFrequency.daily.key, "Daily"), 36 | (openach.models.DigestFrequency.weekly.key, "Weekly"), 37 | ], 38 | default=openach.models.DigestFrequency.daily.key, 39 | verbose_name="email digest frequency", 40 | ), 41 | ), 42 | ( 43 | "user", 44 | models.ForeignKey( 45 | on_delete=django.db.models.deletion.CASCADE, 46 | to=settings.AUTH_USER_MODEL, 47 | ), 48 | ), 49 | ], 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /openach/templates/teams/_notifications.html: -------------------------------------------------------------------------------- 1 | {% load board_extras %} 2 | {% load i18n %} 3 | 4 | {% with obj=notification.action_object target=notification.target actor=notification.actor timesince=notification.timesince %} 5 | 6 | {% url 'profile' notification.actor.id as actor_url %} 7 | 8 | {% if notification.verb == 'remove' %} 9 | {% blocktrans trimmed with team_url=obj.get_absolute_url %} 10 | {{ actor }} removed you from team {{ obj }} {{ timesince }} ago. 11 | {% endblocktrans %} 12 | {% elif notification.verb == 'request_membership' %} 13 | {% blocktrans trimmed with team_url=target.get_absolute_url %} 14 | {{ actor }} requested to join team {{ target }} {{ timesince }} ago. 15 | {% endblocktrans %} 16 | {% elif notification.verb == 'accept' %} 17 | {% blocktrans trimmed with team_url=obj.get_absolute_url %} 18 | {{ actor }} accepted your membership request for team {{ obj }} {{ timesince }} ago. 19 | {% endblocktrans %} 20 | {% elif notification.verb == 'reject' %} 21 | {% blocktrans trimmed with team_url=obj.get_absolute_url %} 22 | {{ actor }} rejected your membership request for team {{ obj }} {{ timesince }} ago. 23 | {% endblocktrans %} 24 | {% elif notification.verb == 'invite' %} 25 | {% blocktrans trimmed with team_url=obj.get_absolute_url %} 26 | {{ actor }} invited you to team {{ obj }} {{ timesince }} ago. 27 | {% endblocktrans %} 28 | {% else %} 29 | {{ notification }} 30 | {% endif %} 31 | 32 | {% endwith %} 33 | -------------------------------------------------------------------------------- /openach/views/util.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import messages 3 | from django.core.exceptions import PermissionDenied 4 | from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator 5 | from django.http import HttpResponseRedirect 6 | from django.urls import reverse 7 | from django.utils.translation import gettext as _ 8 | 9 | 10 | def remove_and_redirect(request, removable, message_detail): 11 | """Mark a model as removed and redirect the user to the associated board detail page.""" 12 | if getattr(settings, "EDIT_REMOVE_ENABLED", True): 13 | removable.removed = True 14 | removable.save() 15 | class_name = ( 16 | removable._meta.verbose_name.title() 17 | ) # pylint: disable=protected-access 18 | class_ = class_name[:1].lower() + class_name[1:] if class_name else "" 19 | messages.success( 20 | request, 21 | _("Removed {object_type}: {detail}").format( 22 | object_type=class_, detail=message_detail 23 | ), 24 | ) # nopep8 25 | return HttpResponseRedirect( 26 | reverse("openach:detail", args=(removable.board.id,)) 27 | ) 28 | else: 29 | raise PermissionDenied() 30 | 31 | 32 | def make_paginator(request, object_list, per_page=10, orphans=3): 33 | """Return a paginator for object_list from request.""" 34 | paginator = Paginator(object_list, per_page=per_page, orphans=orphans) 35 | page = request.GET.get("page") 36 | try: 37 | objects = paginator.page(page) 38 | except PageNotAnInteger: 39 | # if page is not an integer, deliver first page. 40 | objects = paginator.page(1) 41 | except EmptyPage: 42 | # if page is out of range (e.g. 9999), deliver last page of results. 43 | objects = paginator.page(paginator.num_pages) 44 | return objects 45 | -------------------------------------------------------------------------------- /openach/templates/boards/board_audit.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | {% load comments %} 5 | {% load bootstrap %} 6 | 7 | {% block title %} 8 | {% blocktrans trimmed with board_title=board.board_title site_name=site.name %} 9 | Change History for {{ board_title }} | {{ site_name }} 10 | {% endblocktrans %} 11 | {% endblock %} 12 | 13 | {% block content %} 14 | 15 |

16 | {% blocktrans trimmed with board_title=board.board_title %} 17 | Change History: {{ board_title }} 18 | {% endblocktrans %} 19 |

20 | 21 | {% trans "Return to Board" %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for event in history %} 33 | 34 | 35 | 40 | 46 | 47 | {% endfor %} 48 | 49 |
{% trans "Date" %}{% trans "User" %}{% trans "Modification" %}
{{ event.date_created|date }} 36 | {% if event.user %} 37 | {{ event.user.username }} 38 | {% endif %} 39 | 41 | {% get_verbose_field_name event.object event.field_name as field_name %} 42 | {% blocktrans trimmed with field_value=event.field_value %} 43 | Changed {{ field_name }} to {{ field_value }} 44 | {% endblocktrans %} 45 |
50 | 51 | {% endblock content %} 52 | -------------------------------------------------------------------------------- /openach/templates/teams/_details.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 |
5 |
6 | {% if request.user.is_authenticated and request.user == team.owner %} 7 | 10 | {% endif %} 11 |

12 | {% blocktrans trimmed with team_name=team.name %} 13 | Intelligence Team {{ team_name }} 14 | {% endblocktrans %} 15 |

16 |
17 |
18 | {% if team.description %} 19 | {{ team.description }} 20 | {% else %} 21 | {% blocktrans %}This team has no description.{% endblocktrans %} 22 | {% endif %} 23 |
24 |
    25 |
  • 26 | {% if team.creator %} 27 | {% url 'profile' team.creator.id as creator_url %} 28 | {% blocktrans trimmed with creator=team.creator pub_date=team.create_timestamp|date %} 29 | Team created by {{ creator }} on {{ pub_date }}. 30 | {% endblocktrans %} 31 | {% else %} 32 | {% blocktrans trimmed with pub_date=team.create_timestamp|date %} 33 | Team created on {{ pub_date }}. 34 | {% endblocktrans %} 35 | {% endif %} 36 | {% if team.url %} 37 | {% blocktrans with team_url=team.url%} 38 | Team Homepage 39 | {% endblocktrans %} 40 | {% endif %} 41 |
  • 42 |
43 |
44 | -------------------------------------------------------------------------------- /openach/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load bootstrap %} 5 | {% load account socialaccount %} 6 | 7 | {% block title %}{% trans "Sign In" %} | {{ site.name }}{% endblock %} 8 | {% block head_title %}{% trans "Sign In" %}{% endblock %} 9 | 10 | {% block content %} 11 |

{% trans "Sign In" %}

12 | 13 | {% get_providers as socialaccount_providers %} 14 | 15 | {% if socialaccount_providers %} 16 |

{% blocktrans with site.name as site_name %}Please sign in with one 17 | of your existing third party accounts. Or, sign up 18 | for a {{ site_name }} account and sign in below:{% endblocktrans %}

19 | 20 |
21 | 22 |
    23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %} 24 |
25 | 26 | 27 | 28 |
29 | 30 | {% include "socialaccount/snippets/login_extra.html" %} 31 | 32 | {% else %} 33 |

{% blocktrans %}If you have not created an account yet, then please 34 | sign up first.{% endblocktrans %}

35 | {% endif %} 36 | 37 | 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Open Synthesis, an open platform for intelligence analysis 3 | * Copyright (C) 2016-2020 Open Synthesis Contributors. See CONTRIBUTING.md 4 | * file at the top-level directory of this distribution. 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | const { mergeWithCustomize, customizeArray } = require("webpack-merge"); 21 | const common = require("./webpack.config.base.js"); 22 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); 23 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 24 | 25 | module.exports = mergeWithCustomize({ 26 | customizeArray: customizeArray({ 27 | "entry.*": "prepend", 28 | }), 29 | })(common, { 30 | mode: "production", 31 | optimization: { 32 | usedExports: true, 33 | minimizer: [ 34 | "...", // Preserve the native JS minifier 35 | new CssMinimizerPlugin(), 36 | ], 37 | }, 38 | output: { 39 | filename: "[name]-[contenthash].min.js", 40 | chunkFilename: "[name]-[contenthash].bundle.min.js", 41 | }, 42 | plugins: [ 43 | new MiniCssExtractPlugin({ 44 | filename: "css/[name]-[contenthash].min.css", 45 | chunkFilename: "css/[id]-[contenthash].min.css", 46 | ignoreOrder: false, // Enable to remove warnings about conflicting order 47 | }), 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "open-synthesis" 3 | version = "0.0.3" 4 | description = "Open platform for CIA-style intelligence analysis" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "setuptools==80.9.0", 9 | "beautifulsoup4==4.14.2", 10 | "celery==5.5.3", 11 | "dj-database-url==3.0.1", 12 | "django-allauth==0.54.0", 13 | "django-bootstrap-form==3.4", 14 | "django-contrib-comments==2.2.0", 15 | "django-csp==3.8", 16 | "django-environ==0.12.0", 17 | "django-field-history==0.8.0", 18 | "django-invitations==2.1.0", 19 | "django-notifications-hq==1.8.3", 20 | "Django==4.2.27", 21 | "python-slugify==8.0.4", 22 | "qrcode==8.2", 23 | "tldextract==5.3.0", 24 | "django-recaptcha==4.1.0", 25 | "django-sendgrid-v5==1.3.0", 26 | "psycopg2-binary==2.9.11", 27 | "pylibmc==1.6.3", 28 | "redis==7.0.1", 29 | "rollbar==1.3.0", 30 | "gunicorn==23.0.0", 31 | "whitenoise==6.11.0", 32 | "django-webpack-loader==3.2.1", 33 | "nplusone==1.0.0", 34 | ] 35 | 36 | [dependency-groups] 37 | dev = [ 38 | "pytest==9.0.2", 39 | "coverage==7.11.0", 40 | "pytest-django==4.11.1", 41 | "pytest-cov==7.0.0", 42 | "pre-commit==4.4.0", 43 | "django-debug-toolbar==6.1.0", 44 | "django-coverage-plugin==3.2.0", 45 | "factory-boy==3.3.3", 46 | ] 47 | coverage = [ 48 | "coveralls==4.0.2" 49 | ] 50 | 51 | [tool.pre-commit] 52 | version = "4.2.0" 53 | 54 | [tool.isort] 55 | multi_line_output = 3 56 | include_trailing_comma = true 57 | force_grid_wrap = 0 58 | use_parentheses = true 59 | line_length = 88 60 | known_third_party = ["allauth", "bs4", "celery", "csp", "dj_database_url", "django", "django_comments", "environ", "factory", "field_history", "gunicorn", "invitations", "notifications", "qrcode", "requests", "slugify", "tldextract", "urllib3"] 61 | 62 | [tool.pytest.ini_options] 63 | pythonpath = ["."] 64 | DJANGO_SETTINGS_MODULE = "openintel.settings" 65 | python_files = "tests.py test_*.py *_tests.py" 66 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Open Synthesis, an open platform for intelligence analysis 3 | * Copyright (C) 2016-2020 Open Synthesis Contributors. See CONTRIBUTING.md 4 | * file at the top-level directory of this distribution. 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | const path = require("path"); 21 | const { mergeWithCustomize, customizeArray } = require("webpack-merge"); 22 | const common = require("./webpack.config.base.js"); 23 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 24 | 25 | module.exports = mergeWithCustomize({ 26 | customizeArray: customizeArray({ 27 | "entry.*": "prepend", 28 | }), 29 | })(common, { 30 | mode: "development", 31 | devtool: "inline-source-map", 32 | // https://webpack.js.org/configuration/watch/ 33 | // https://webpack.js.org/configuration/watch/#saving-in-webstorm 34 | watchOptions: { 35 | ignored: /node_modules/, 36 | }, 37 | output: { 38 | filename: "[name].js", 39 | chunkFilename: "[name].bundle.js", 40 | }, 41 | plugins: [ 42 | new MiniCssExtractPlugin({ 43 | path: path.resolve("./openach-frontend/bundles/css/"), 44 | filename: "css/[name].css", 45 | chunkFilename: "css/[id].css", 46 | ignoreOrder: false, // Enable to remove warnings about conflicting order 47 | }), 48 | ], 49 | optimization: { 50 | minimize: false, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /openach/migrations/0046_auto_20180908_0056.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2018-09-08 00:56 2 | import django.db.models.deletion 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("openach", "0045_auto_20180331_2113"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="board", 16 | name="creator", 17 | field=models.ForeignKey( 18 | null=True, 19 | on_delete=django.db.models.deletion.SET_NULL, 20 | to=settings.AUTH_USER_MODEL, 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="evidence", 25 | name="creator", 26 | field=models.ForeignKey( 27 | null=True, 28 | on_delete=django.db.models.deletion.SET_NULL, 29 | to=settings.AUTH_USER_MODEL, 30 | ), 31 | ), 32 | migrations.AlterField( 33 | model_name="evidencesource", 34 | name="uploader", 35 | field=models.ForeignKey( 36 | null=True, 37 | on_delete=django.db.models.deletion.SET_NULL, 38 | to=settings.AUTH_USER_MODEL, 39 | ), 40 | ), 41 | migrations.AlterField( 42 | model_name="hypothesis", 43 | name="creator", 44 | field=models.ForeignKey( 45 | null=True, 46 | on_delete=django.db.models.deletion.SET_NULL, 47 | to=settings.AUTH_USER_MODEL, 48 | ), 49 | ), 50 | migrations.AlterField( 51 | model_name="projectnews", 52 | name="author", 53 | field=models.ForeignKey( 54 | null=True, 55 | on_delete=django.db.models.deletion.SET_NULL, 56 | to=settings.AUTH_USER_MODEL, 57 | ), 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /openach-frontend/src/css/sharing.css: -------------------------------------------------------------------------------- 1 | /* Adapted from http://crunchify.com/how-to-create-social-sharing-button-without-any-plugin-and-script-loading-wordpress-speed-optimization-goal/ */ 2 | 3 | @media screen and (min-width: 1024px) { 4 | /* Hide WhatsApp sharing link on the Desktop */ 5 | .share-whatsapp { 6 | display: none !important; 7 | } 8 | } 9 | 10 | .share-link { 11 | padding: 2px 8px 4px 8px !important; 12 | color: white; 13 | font-size: 12px; 14 | border-radius: 2px; 15 | margin-right: 2px; 16 | cursor: pointer; 17 | -moz-background-clip: padding; 18 | -webkit-background-clip: padding-box; 19 | box-shadow: inset 0 -3px 0 rgba(0,0,0,.2); 20 | -moz-box-shadow: inset 0 -3px 0 rgba(0,0,0,.2); 21 | -webkit-box-shadow: inset 0 -3px 0 rgba(0,0,0,.2); 22 | margin-top: 2px; 23 | display: inline-block; 24 | text-decoration: none; 25 | } 26 | 27 | .share-link:hover,.share-link:active { 28 | color: white; 29 | } 30 | 31 | .share-twitter { 32 | background: #00aced; 33 | } 34 | 35 | .share-twitter:hover,.share-twitter:active { 36 | background: #0084b4; 37 | } 38 | 39 | .share-facebook { 40 | background: #3B5997; 41 | } 42 | 43 | .share-facebook:hover,.share-facebook:active { 44 | background: #2d4372; 45 | } 46 | 47 | .share-email { 48 | background: #444; 49 | } 50 | 51 | .share-email:hover,.share-email:active { 52 | background: #222; 53 | } 54 | 55 | .share-reddit { 56 | background: #0074A1; 57 | } 58 | 59 | .share-reddit:hover,.share-reddit:active { 60 | background: #006288; 61 | } 62 | 63 | .share-whatsapp { 64 | background: #43d854; 65 | } 66 | 67 | .share-whatsapp:hover,.share-whatsapp:active { 68 | background: #009688; 69 | } 70 | 71 | .share-social { 72 | margin: 20px 0px 25px 0px; 73 | -webkit-font-smoothing: antialiased; 74 | font-size: 12px; 75 | } 76 | 77 | .share-hackernews { 78 | background: #D64937; 79 | } 80 | 81 | .share-hackernews:hover,.share-hackernews:active { 82 | background: #b53525; 83 | } 84 | -------------------------------------------------------------------------------- /openach/templates/teams/manage_team.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | {% load notifications_tags %} 5 | {% load bootstrap %} 6 | 7 | {% block title %}{% trans "Team" %} | {{ site.name }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 | {% include 'teams/_details.html' %} 13 | 14 |
15 |
16 | 19 |

{% trans "Membership Requests" %}

20 |
21 |
    22 | {% for invite in member_requests %} 23 |
  • 24 |
    25 | {% csrf_token %} 26 | {{ invite.invitee.username }} 27 |
    28 | 29 | 30 |
    31 |
    32 |
  • 33 | {% empty %} 34 |
  • {% trans "No membership requests" %}
  • 35 | {% endfor %} 36 |
37 |
38 |
39 | 40 |
41 | {% include 'teams/_member_panel.html' %} 42 |
43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /openintel/celery.py: -------------------------------------------------------------------------------- 1 | """Celery configuration. 2 | 3 | For more information, please see: 4 | - http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html#first-steps-with-celery 5 | - http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html 6 | - https://devcenter.heroku.com/articles/celery-heroku 7 | """ 8 | 9 | import logging 10 | import os 11 | 12 | from celery import Celery 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openintel.settings") 15 | 16 | from django.conf import settings # noqa 17 | 18 | logger = logging.getLogger(__name__) # pylint: disable=invalid-name 19 | app = Celery("openintel") # pylint: disable=invalid-name 20 | 21 | app.conf.update( 22 | CELERY_TASK_SERIALIZER="json", 23 | CELERY_ACCEPT_CONTENT=["json"], # Ignore other content 24 | CELERY_RESULT_SERIALIZER="json", 25 | # synchronously execute tasks, so you don't need to run a celery server 26 | # http://docs.celeryproject.org/en/latest/configuration.html#celery-always-eager 27 | CELERY_ALWAYS_EAGER=getattr(settings, "CELERY_ALWAYS_EAGER", False), 28 | ) 29 | 30 | # Using a string here means the worker will not have to 31 | # pickle the object when using Windows. 32 | app.config_from_object("django.conf:settings") 33 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 34 | 35 | _BROKER_URL = getattr(settings, "CELERY_BROKER_URL", None) 36 | _BACKEND_URL = getattr(settings, "CELERY_RESULT_BACKEND", _BROKER_URL) 37 | 38 | 39 | if app.conf.CELERY_ALWAYS_EAGER: 40 | logger.warning("Running Celery tasks eagerly/synchronously; may impact performance") 41 | if _BROKER_URL or _BACKEND_URL: 42 | logger.warning("Ignoring Celery broker and result backend settings") 43 | elif _BROKER_URL and _BACKEND_URL: 44 | logger.info("Celery Broker: %s", _BROKER_URL) 45 | logger.info("Celery Result Backend: %s", _BACKEND_URL) 46 | app.conf.update(BROKER_URL=_BROKER_URL, CELERY_RESULT_BACKEND=_BACKEND_URL) 47 | else: 48 | logger.warning( 49 | "No broker url/backend supplied for Celery; enable CELERY_ALWAYS_EAGER to run without a server" 50 | ) 51 | -------------------------------------------------------------------------------- /openach/migrations/0015_auto_20160915_1402.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-15 14:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0014_auto_20160908_1915"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="evidence", 15 | options={"verbose_name_plural": "evidence"}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name="hypothesis", 19 | options={"verbose_name_plural": "hypotheses"}, 20 | ), 21 | migrations.AddField( 22 | model_name="board", 23 | name="removed", 24 | field=models.BooleanField(default=False), 25 | preserve_default=False, 26 | ), 27 | migrations.AddField( 28 | model_name="evidence", 29 | name="removed", 30 | field=models.BooleanField(default=False), 31 | preserve_default=False, 32 | ), 33 | migrations.AddField( 34 | model_name="hypothesis", 35 | name="removed", 36 | field=models.BooleanField(default=False), 37 | preserve_default=False, 38 | ), 39 | migrations.AlterField( 40 | model_name="board", 41 | name="board_desc", 42 | field=models.CharField(max_length=255, verbose_name="board description"), 43 | ), 44 | migrations.AlterField( 45 | model_name="evidence", 46 | name="event_date", 47 | field=models.DateField(null=True, verbose_name="evidence event date"), 48 | ), 49 | migrations.AlterField( 50 | model_name="evidence", 51 | name="evidence_desc", 52 | field=models.CharField(max_length=200, verbose_name="evidence description"), 53 | ), 54 | migrations.AlterField( 55 | model_name="hypothesis", 56 | name="hypothesis_text", 57 | field=models.CharField(max_length=200, verbose_name="hypothesis"), 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /openach/tests/test_sitemap.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | from openach.models import Evidence, Hypothesis 5 | from openach.sitemap import BoardSitemap 6 | 7 | from .common import PrimaryUserTestCase, create_board, remove 8 | 9 | 10 | class SitemapTests(PrimaryUserTestCase): 11 | def setUp(self): 12 | super().setUp() 13 | self.board = create_board("Test Board", days=-5) 14 | self.evidence = Evidence.objects.create( 15 | board=self.board, 16 | creator=self.user, 17 | evidence_desc="Evidence #1", 18 | event_date=None, 19 | ) 20 | self.hypotheses = [ 21 | Hypothesis.objects.create( 22 | board=self.board, 23 | hypothesis_text="Hypothesis #1", 24 | creator=self.user, 25 | ) 26 | ] 27 | 28 | def test_can_get_items(self): 29 | """Test that we can get all the boards.""" 30 | sitemap = BoardSitemap() 31 | self.assertEqual(len(sitemap.items()), 1, "Sitemap included removed board") 32 | 33 | def test_cannot_get_removed_items(self): 34 | """Test that the sitemap doesn't include removed boards.""" 35 | remove(self.board) 36 | sitemap = BoardSitemap() 37 | self.assertEqual(len(sitemap.items()), 0) 38 | 39 | def test_can_get_last_update(self): 40 | """Test that sitemap uses the latest change.""" 41 | latest = Hypothesis.objects.create( 42 | board=self.board, 43 | hypothesis_text="Hypothesis #2", 44 | creator=self.user, 45 | ) 46 | sitemap = BoardSitemap() 47 | board = sitemap.items()[0] 48 | self.assertEqual(sitemap.lastmod(board), latest.submit_date) 49 | 50 | 51 | class RobotsViewTests(TestCase): 52 | def test_can_render_robots_page(self): 53 | """Test that the robots.txt view returns a robots.txt that includes a sitemap.""" 54 | response = self.client.get(reverse("robots")) 55 | self.assertTemplateUsed(response, "robots.txt") 56 | self.assertContains(response, "sitemap.xml", status_code=200) 57 | self.assertEqual(response["Content-Type"], "text/plain") 58 | -------------------------------------------------------------------------------- /.github/workflows/integration.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | CI: true 11 | DEBUG: true 12 | SITE_NAME: Open Synthesis (CI) 13 | SITE_DOMAIN: localhost 14 | DJANGO_SECRET_KEY: DONOTUSEINPRODUCTION 15 | ROLLBAR_ENABLED: false 16 | APP_LOG_LEVEL: DEBUG 17 | ALLOWED_HOSTS: 127.0.0.1 18 | ENVIRONMENT: development 19 | SECURE_SSL_REDIRECT: false 20 | SESSION_COOKIE_SECURE: false 21 | CSRF_COOKIE_SECURE: false 22 | CSRF_COOKIE_HTTPONLY: true 23 | ENABLE_CACHE: false 24 | ACCOUNT_REQUIRED: false 25 | ADMIN_EMAIL_ADDRESS: "admin@localhost" 26 | CELERY_ALWAYS_EAGER: true 27 | 28 | steps: 29 | - uses: actions/checkout@v6 30 | # https://docs.astral.sh/uv/guides/integration/github/#installation 31 | - name: Install uv 32 | uses: astral-sh/setup-uv@v7 33 | with: 34 | enable-cache: true 35 | cache-dependency-glob: "uv.lock" 36 | - uses: actions/setup-node@v6 37 | with: 38 | cache: npm 39 | - name: "Set up Python" 40 | uses: actions/setup-python@v6 41 | with: 42 | python-version-file: ".python-version" 43 | - name: Install development headers 44 | run: | 45 | sudo apt-get install libmemcached-dev 46 | - name: Install the project 47 | run: uv sync --all-extras --dev --group=coverage 48 | - name: Build Front-end 49 | # `npm ci` also runs `npm run build` because it's in the `post-install` action in package.json 50 | run: | 51 | npm ci 52 | uv run manage.py collectstatic --noinput 53 | - name: Django system checks 54 | run: | 55 | uv run manage.py check 56 | uv run manage.py makemigrations --check --dry-run 57 | - name: Run Tests 58 | run: uv run pytest --cov='.' 59 | - name: Upload Coverage 60 | # https://coveralls-python.readthedocs.io/en/latest/usage/configuration.html#github-actions-support 61 | run: | 62 | uv run coveralls --service=github 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /openach/views/notifications.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib import messages 4 | from django.contrib.auth.decorators import login_required 5 | from django.http import HttpResponseRedirect 6 | from django.shortcuts import render 7 | from django.utils.translation import gettext as _ 8 | from django.views.decorators.http import require_http_methods, require_safe 9 | from notifications.signals import notify 10 | 11 | from .util import make_paginator 12 | 13 | logger = logging.getLogger(__name__) # pylint: disable=invalid-name 14 | 15 | 16 | @require_safe 17 | @login_required 18 | def notifications(request): 19 | """Return a paginated list of notifications for the user.""" 20 | notification_list = request.user.notifications.unread() 21 | context = { 22 | "notifications": make_paginator(request, notification_list), 23 | } 24 | return render(request, "boards/notifications/notifications.html", context) 25 | 26 | 27 | @require_http_methods(["HEAD", "GET", "POST"]) 28 | @login_required 29 | def clear_notifications(request): 30 | """Handle POST request to clear notifications and redirect user to their profile.""" 31 | if request.method == "POST": 32 | if "clear" in request.POST: 33 | request.user.notifications.mark_all_as_read() 34 | messages.success(request, _("Cleared all notifications.")) 35 | return HttpResponseRedirect("/accounts/profile") 36 | 37 | 38 | def notify_followers(board, actor, verb, action_object): 39 | """Notify board followers of that have read permissions for the board.""" 40 | for follow in board.followers.all().select_related("user"): 41 | if follow.user != actor and board.can_read(follow.user): 42 | notify.send( 43 | actor, 44 | recipient=follow.user, 45 | actor=actor, 46 | verb=verb, 47 | action_object=action_object, 48 | target=board, 49 | ) 50 | 51 | 52 | def notify_add(board, actor, action_object): 53 | """Notify board followers of an addition.""" 54 | notify_followers(board, actor, "added", action_object) 55 | 56 | 57 | def notify_edit(board, actor, action_object): 58 | """Notify board followers of an edit.""" 59 | notify_followers(board, actor, "edited", action_object) 60 | -------------------------------------------------------------------------------- /openach/migrations/0037_auto_20180107_2344.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.6 on 2018-01-07 23:44 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("openach", "0036_defaultpermissions"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name="board", 16 | options={"ordering": ("-pub_date",)}, 17 | ), 18 | migrations.AddField( 19 | model_name="boardpermissions", 20 | name="edit_board", 21 | field=models.PositiveSmallIntegerField( 22 | choices=[ 23 | (0, "Only Me"), 24 | (1, "Collaborators"), 25 | (2, "Registered Users"), 26 | (3, "Public"), 27 | ], 28 | default=0, 29 | help_text="Who can edit the board title, description, and permissions?", 30 | ), 31 | ), 32 | migrations.AlterField( 33 | model_name="boardpermissions", 34 | name="add_comments", 35 | field=models.PositiveSmallIntegerField( 36 | choices=[ 37 | (0, "Only Me"), 38 | (1, "Collaborators"), 39 | (2, "Registered Users"), 40 | (3, "Public"), 41 | ], 42 | default=1, 43 | help_text="Who can comment on the board?", 44 | ), 45 | ), 46 | migrations.AlterField( 47 | model_name="boardpermissions", 48 | name="collaborators", 49 | field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), 50 | ), 51 | migrations.AlterField( 52 | model_name="boardpermissions", 53 | name="edit_elements", 54 | field=models.PositiveSmallIntegerField( 55 | choices=[ 56 | (0, "Only Me"), 57 | (1, "Collaborators"), 58 | (2, "Registered Users"), 59 | (3, "Public"), 60 | ], 61 | default=1, 62 | help_text="Who can edit hypotheses, evidence, and sources?", 63 | ), 64 | ), 65 | ] 66 | -------------------------------------------------------------------------------- /openach/static/boards/images/planet-earth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 11 | 12 | 14 | 15 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /openach/templates/boards/email/_notification.html: -------------------------------------------------------------------------------- 1 | {% load board_extras %} 2 | {% load i18n %} 3 | 4 | {% if notification.verb == 'added' %} 5 | {% if notification.action_object|get_class == 'Hypothesis' %} 6 | {% blocktrans trimmed with site_domain=site.domain actor_id=notification.actor.id actor_username=notification.actor.username action_name=notification.action_object %} 7 | {{ actor_username }} added hypothesis '{{ action_name }}' 8 | {% endblocktrans %} 9 | {% elif notification.action_object|get_class == 'Evidence' %} 10 | {% url 'openach:evidence_detail' notification.action_object.id as evidence_detail_url %} 11 | {% blocktrans trimmed with site_domain=site.domain actor_id=notification.actor.id actor_username=notification.actor.username action_name=notification.action_object %} 12 | {{ actor_username }} 13 | added evidence 14 | 15 | '{{ action_name }}' 16 | 17 | {% endblocktrans %} 18 | {% else %} 19 | {{ notification }} 20 | {% endif %} 21 | {% elif notification.verb == 'edited' %} 22 | {% if notification.action_object|get_class == 'Hypothesis' %} 23 | {% blocktrans trimmed with site_domain=site.domain actor_id=notification.actor.id actor_username=notification.actor.username action_name=notification.action_object %} 24 | {{ actor_username }} edited hypothesis '{{ action_name }}' 25 | {% endblocktrans %} 26 | {% elif notification.action_object|get_class == 'Evidence' %} 27 | {% url 'openach:evidence_detail' notification.action_object.id as evidence_detail_url %} 28 | {% blocktrans trimmed with site_domain=site.domain actor_id=notification.actor.id actor_username=notification.actor.username action_name=notification.action_object %} 29 | {{ actor_username }} 30 | edited evidence 31 | 32 | '{{ action_name }}' 33 | 34 | {% endblocktrans %} 35 | {% else %} 36 | {{ notification }} 37 | {% endif %} 38 | {% else %} 39 | {{ notification }} 40 | {% endif %} 41 | -------------------------------------------------------------------------------- /openach/tests/factories.py: -------------------------------------------------------------------------------- 1 | # Open Synthesis, an open platform for intelligence analysis 2 | # Copyright (C) 2016-2020 Open Synthesis Contributors. See CONTRIBUTING.md 3 | # file at the top-level directory of this distribution. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import factory 18 | from django.contrib.auth import get_user_model 19 | from django.utils import timezone 20 | from factory.django import DjangoModelFactory 21 | 22 | from openach import models 23 | 24 | User = get_user_model() 25 | 26 | 27 | class BoardFactory(DjangoModelFactory): 28 | class Meta: 29 | model = models.Board 30 | 31 | board_title = factory.Sequence(lambda x: f"Board Title {x}") 32 | 33 | pub_date = factory.LazyFunction(timezone.now) 34 | 35 | board_desc = "Description" 36 | 37 | @factory.post_generation 38 | def teams(obj, create, extracted, **kwargs): 39 | if not create: 40 | return 41 | if extracted: 42 | obj.permissions.teams.set(extracted, clear=True) 43 | 44 | @factory.post_generation 45 | def permissions(obj, create, extracted, **kwargs): 46 | if not create: 47 | return 48 | if extracted: 49 | obj.permissions.update_all(extracted) 50 | 51 | 52 | class TeamFactory(DjangoModelFactory): 53 | class Meta: 54 | model = models.Team 55 | 56 | name = factory.Sequence(lambda x: f"Team {x}") 57 | 58 | @factory.post_generation 59 | def members(obj, create, extracted, **kwargs): 60 | if not create: 61 | return 62 | if extracted: 63 | obj.members.set([obj.owner, *extracted], clear=True) 64 | 65 | 66 | class UserFactory(DjangoModelFactory): 67 | class Meta: 68 | model = models.User 69 | 70 | username = factory.Sequence(lambda x: f"username{x}") 71 | -------------------------------------------------------------------------------- /openach/tests/common.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.test import Client, TestCase 5 | from django.utils import timezone 6 | from field_history.tracker import FieldHistoryTracker 7 | 8 | from openach.models import Board, BoardFollower 9 | 10 | PASSWORD = "commonpassword" 11 | USERNAME_PRIMARY = "john" 12 | USERNAME_OTHER = "paul" 13 | 14 | HTTP_OK = 200 15 | HTTP_FORBIDDEN = 403 16 | HTTP_REDIRECT = 302 17 | 18 | User = get_user_model() 19 | 20 | 21 | def create_board(board_title, days=0, public=True): 22 | """Create a board with the given title and publishing date offset. 23 | 24 | :param board_title: the board title 25 | :param days: negative for boards published in the past, positive for boards that have yet to be published 26 | :param public: true if all permissions should be set to public (see make_public) 27 | """ 28 | time = timezone.now() + datetime.timedelta(days=days) 29 | board = Board.objects.create(board_title=board_title, pub_date=time) 30 | if public: 31 | board.permissions.make_public() 32 | return board 33 | 34 | 35 | def remove(model): 36 | """Mark model as removed.""" 37 | model.removed = True 38 | model.save() 39 | 40 | 41 | def add_follower(board): 42 | """Create a user and have the user follow the given board.""" 43 | follower = User.objects.create_user("bob", "bob@thebeatles.com", "bobpassword") 44 | BoardFollower.objects.create( 45 | user=follower, 46 | board=board, 47 | ) 48 | return follower 49 | 50 | 51 | class PrimaryUserTestCase(TestCase): 52 | def assertStatus(self, response, expected_status): 53 | self.assertEqual(response.status_code, expected_status) 54 | 55 | def setUp(self): 56 | self.client = Client() 57 | self.user = User.objects.create_user( 58 | USERNAME_PRIMARY, f"{USERNAME_PRIMARY}@thebeatles.com", PASSWORD 59 | ) 60 | self.other = User.objects.create_user( 61 | USERNAME_OTHER, f"{USERNAME_OTHER}@thebeatles.com", PASSWORD 62 | ) 63 | 64 | def tearDown(self) -> None: 65 | FieldHistoryTracker.thread.request = None 66 | 67 | def login(self): 68 | self.client.login(username=USERNAME_PRIMARY, password=PASSWORD) 69 | 70 | def login_other(self): 71 | self.client.login(username=USERNAME_OTHER, password=PASSWORD) 72 | 73 | def logout(self): 74 | self.client.logout() 75 | -------------------------------------------------------------------------------- /openach/migrations/0007_auto_20160826_1841.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-26 18:41 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("openach", "0006_evidence_submit_date"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="AnalystSourceTag", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("tag_date", models.DateTimeField(verbose_name="date tagged")), 29 | ( 30 | "source", 31 | models.ForeignKey( 32 | on_delete=django.db.models.deletion.CASCADE, 33 | to="openach.EvidenceSource", 34 | ), 35 | ), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name="EvidenceSourceTag", 40 | fields=[ 41 | ( 42 | "id", 43 | models.AutoField( 44 | auto_created=True, 45 | primary_key=True, 46 | serialize=False, 47 | verbose_name="ID", 48 | ), 49 | ), 50 | ("tag_name", models.CharField(max_length=64)), 51 | ("tag_desc", models.CharField(max_length=200)), 52 | ], 53 | ), 54 | migrations.AddField( 55 | model_name="analystsourcetag", 56 | name="tag", 57 | field=models.ForeignKey( 58 | on_delete=django.db.models.deletion.CASCADE, 59 | to="openach.EvidenceSourceTag", 60 | ), 61 | ), 62 | migrations.AddField( 63 | model_name="analystsourcetag", 64 | name="tagger", 65 | field=models.ForeignKey( 66 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 67 | ), 68 | ), 69 | ] 70 | -------------------------------------------------------------------------------- /openach/migrations/0039_auto_20180111_0248.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.6 on 2018-01-11 02:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("openach", "0038_auto_20180108_0022"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="boardpermissions", 15 | name="add_comments", 16 | field=models.PositiveSmallIntegerField( 17 | choices=[(0, "Only Me"), (1, "Collaborators"), (2, "Registered Users")], 18 | default=1, 19 | help_text="Who can comment on the board?", 20 | ), 21 | ), 22 | migrations.AlterField( 23 | model_name="boardpermissions", 24 | name="add_elements", 25 | field=models.PositiveSmallIntegerField( 26 | choices=[(0, "Only Me"), (1, "Collaborators"), (2, "Registered Users")], 27 | default=1, 28 | help_text="Who can add hypotheses, evidence, and sources?", 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="boardpermissions", 33 | name="edit_board", 34 | field=models.PositiveSmallIntegerField( 35 | choices=[(0, "Only Me"), (1, "Collaborators"), (2, "Registered Users")], 36 | default=0, 37 | help_text="Who can edit the board title, description, and permissions?", 38 | ), 39 | ), 40 | migrations.AlterField( 41 | model_name="boardpermissions", 42 | name="edit_elements", 43 | field=models.PositiveSmallIntegerField( 44 | choices=[(0, "Only Me"), (1, "Collaborators"), (2, "Registered Users")], 45 | default=1, 46 | help_text="Who can edit hypotheses, evidence, and sources?", 47 | ), 48 | ), 49 | migrations.AlterField( 50 | model_name="boardpermissions", 51 | name="read_board", 52 | field=models.PositiveSmallIntegerField( 53 | choices=[ 54 | (0, "Only Me"), 55 | (1, "Collaborators"), 56 | (2, "Registered Users"), 57 | (3, "Public"), 58 | ], 59 | default=3, 60 | help_text="Who can view the board?", 61 | ), 62 | ), 63 | ] 64 | -------------------------------------------------------------------------------- /openach/decorators.py: -------------------------------------------------------------------------------- 1 | """Analysis of Competing Hypotheses View Decorators for managing caching and authorization.""" 2 | 3 | from functools import wraps 4 | 5 | from django.conf import settings 6 | from django.contrib import messages 7 | from django.contrib.auth.decorators import REDIRECT_FIELD_NAME, user_passes_test 8 | from django.views.decorators.cache import cache_page 9 | 10 | 11 | def account_required( 12 | func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None 13 | ): 14 | """Require that the (1) the user is logged in, or (2) that an account is not required to view the page. 15 | 16 | If the user fails the test, redirect the user to the log-in page. See also 17 | django.contrib.auth.decorators.login_required 18 | """ 19 | req = getattr(settings, "ACCOUNT_REQUIRED", False) 20 | actual_decorator = user_passes_test( 21 | lambda u: not req or u.is_authenticated, 22 | login_url=login_url, 23 | redirect_field_name=redirect_field_name, 24 | ) 25 | if func: 26 | return actual_decorator(func) 27 | return actual_decorator 28 | 29 | 30 | def cache_on_auth(timeout): 31 | """Cache the response based on whether or not the user is authenticated. 32 | 33 | Do NOT use on views that include user-specific information, e.g., CSRF tokens. 34 | """ 35 | 36 | # https://stackoverflow.com/questions/11661503/django-caching-for-authenticated-users-only 37 | def _decorator(view_func): 38 | @wraps(view_func) 39 | def _wrapped_view(request, *args, **kwargs): 40 | key_prefix = "_auth_%s_" % request.user.is_authenticated 41 | return cache_page(timeout, key_prefix=key_prefix)(view_func)( 42 | request, *args, **kwargs 43 | ) 44 | 45 | return _wrapped_view 46 | 47 | return _decorator 48 | 49 | 50 | def cache_if_anon(timeout): 51 | """Cache the view if the user is not authenticated and there are no messages to display.""" 52 | 53 | # https://stackoverflow.com/questions/11661503/django-caching-for-authenticated-users-only 54 | def _decorator(view_func): 55 | @wraps(view_func) 56 | def _wrapped_view(request, *args, **kwargs): 57 | if request.user.is_authenticated or messages.get_messages(request): 58 | return view_func(request, *args, **kwargs) 59 | else: 60 | return cache_page(timeout)(view_func)(request, *args, **kwargs) 61 | 62 | return _wrapped_view 63 | 64 | return _decorator 65 | -------------------------------------------------------------------------------- /openach/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10 on 2016-08-24 18:12 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Board", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("board_title", models.CharField(max_length=200)), 27 | ("board_desc", models.CharField(max_length=200)), 28 | ("pub_date", models.DateTimeField(verbose_name="date published")), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name="Evidence", 33 | fields=[ 34 | ( 35 | "id", 36 | models.AutoField( 37 | auto_created=True, 38 | primary_key=True, 39 | serialize=False, 40 | verbose_name="ID", 41 | ), 42 | ), 43 | ("evidence_url", models.URLField()), 44 | ( 45 | "board", 46 | models.ForeignKey( 47 | on_delete=django.db.models.deletion.CASCADE, to="openach.Board" 48 | ), 49 | ), 50 | ], 51 | ), 52 | migrations.CreateModel( 53 | name="Hypothesis", 54 | fields=[ 55 | ( 56 | "id", 57 | models.AutoField( 58 | auto_created=True, 59 | primary_key=True, 60 | serialize=False, 61 | verbose_name="ID", 62 | ), 63 | ), 64 | ("hypothesis_text", models.CharField(max_length=200)), 65 | ( 66 | "board", 67 | models.ForeignKey( 68 | on_delete=django.db.models.deletion.CASCADE, to="openach.Board" 69 | ), 70 | ), 71 | ], 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /openach/management/commands/senddigest.py: -------------------------------------------------------------------------------- 1 | """Django admin command to send email digests.""" 2 | 3 | from django.conf import settings 4 | from django.core.management.base import BaseCommand, CommandError 5 | from django.utils import timezone 6 | 7 | from openach.digest import send_digest_emails 8 | from openach.models import DigestFrequency 9 | 10 | 11 | class Command(BaseCommand): 12 | """Django admin command to send out digest emails.""" 13 | 14 | help = "Send digest emails to users subscribed with the given frequency." 15 | 16 | def add_arguments(self, parser): 17 | """Add 'frequency' and 'force' command arguments.""" 18 | parser.add_argument("frequency", choices=["daily", "weekly"]) 19 | parser.add_argument( 20 | "--force", 21 | nargs="?", 22 | dest="force", 23 | const=True, 24 | default=False, 25 | help="Force the weekly digest to be sent regardless of day (default: False)", 26 | ) 27 | 28 | def report(self, cnt): 29 | """Report number of emails sent to STDOUT.""" 30 | msg = "Sent %s digest emails (%s skipped, %s failed)" % cnt 31 | self.stdout.write(self.style.SUCCESS(msg)) # pylint: disable=no-member 32 | 33 | def handle(self, *args, **options): 34 | """Handle the command invocation.""" 35 | if options["frequency"] == "daily": 36 | self.report(send_digest_emails(DigestFrequency.daily)) 37 | elif options["frequency"] == "weekly": 38 | digest_day = getattr(settings, "DIGEST_WEEKLY_DAY") 39 | current_day = timezone.now().weekday() 40 | if current_day == digest_day or options["force"]: 41 | if current_day != digest_day and options["force"]: 42 | msg = ( 43 | "Forcing weekly digest to be sent (scheduled=%s, current=%s)" 44 | % (digest_day, current_day) 45 | ) 46 | self.stdout.write( 47 | self.style.WARNING(msg) 48 | ) # pylint: disable=no-member 49 | self.report(send_digest_emails(DigestFrequency.weekly)) 50 | else: 51 | msg = "Skipping weekly digest until day {} (current={})".format( 52 | digest_day, 53 | current_day, 54 | ) 55 | self.stdout.write(self.style.WARNING(msg)) # pylint: disable=no-member 56 | else: 57 | raise CommandError('Expected frequency "daily" or "weekly"') 58 | -------------------------------------------------------------------------------- /openach/tasks.py: -------------------------------------------------------------------------------- 1 | """Celery tasks. 2 | 3 | For more information, please see: 4 | - http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html 5 | 6 | """ 7 | 8 | import logging 9 | import re 10 | 11 | import requests 12 | from bs4 import BeautifulSoup 13 | from celery import shared_task 14 | from requests.adapters import HTTPAdapter 15 | from urllib3.util.retry import Retry 16 | 17 | from .models import EvidenceSource 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | SOURCE_METADATA_RETRY = Retry( 22 | total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504] 23 | ) 24 | 25 | 26 | @shared_task 27 | def example_task(x, y): # pragma: no cover 28 | """Add two numbers together. 29 | 30 | An example for reference. 31 | """ 32 | return x + y 33 | 34 | 35 | def parse_metadata(html): 36 | """Return document metadata from Open Graph, meta, and title tags. 37 | 38 | If title Open Graph property is not set, uses the document title tag. If the description Open Graph property is 39 | not set, uses the description meta tag. 40 | 41 | For more information, please see: 42 | - http://ogp.me/ 43 | """ 44 | # adapted from https://github.com/erikriver/opengraph 45 | metadata = {} 46 | doc = BeautifulSoup(html, "html.parser") 47 | tags = doc.html.head.find_all(property=re.compile(r"^og")) 48 | for tag in tags: 49 | if tag.has_attr("content"): 50 | metadata[tag["property"][len("og:") :]] = tag["content"].strip() 51 | 52 | if "title" not in metadata and doc.title: 53 | metadata["title"] = doc.title.text.strip() 54 | 55 | if "description" not in metadata: 56 | description_tags = doc.html.head.find_all("meta", attrs={"name": "description"}) 57 | # there should be at most one description tag per document 58 | if len(description_tags) > 0: 59 | metadata["description"] = description_tags[0].get("content", "").strip() 60 | 61 | return metadata 62 | 63 | 64 | @shared_task 65 | def fetch_source_metadata(source_id): 66 | """Fetch title and description metadata for the given source.""" 67 | source = EvidenceSource.objects.get(id=source_id) 68 | session = requests.session() 69 | session.mount("http://", HTTPAdapter(max_retries=SOURCE_METADATA_RETRY)) 70 | session.mount("https://", HTTPAdapter(max_retries=SOURCE_METADATA_RETRY)) 71 | html = session.get(source.source_url) 72 | metadata = parse_metadata(html.text) 73 | source.source_title = metadata.get("title", "") 74 | source.source_description = metadata.get("description", "") 75 | source.save() 76 | -------------------------------------------------------------------------------- /openach/templates/boards/notifications/_boards.html: -------------------------------------------------------------------------------- 1 | {% load board_extras %} 2 | {% load i18n %} 3 | 4 | {% if notification.verb == 'added' %} 5 | {% if notification.action_object|get_class == 'Hypothesis' %} 6 | {% blocktrans trimmed with notification.actor.id as actor_id and notification.actor.username as actor_username and notification.action_object as action_name and notification.target|board_url as custom_url and notification.target as target and notification.timesince as timesince %} 7 | {{ actor_username }} 8 | added hypothesis '{{ action_name }}' to board {{ target }} {{ timesince }} ago. 9 | {% endblocktrans %} 10 | {% elif notification.action_object|get_class == 'Evidence' %} 11 | {% blocktrans trimmed with notification.actor.id as actor_id and notification.actor.username as actor_username and notification.action_object as action_name and notification.target|board_url as custom_url and notification.target as target and notification.timesince as timesince %} 12 | {{ actor_username }} 13 | added evidence '{{ action_name }}' to board {{ target }} {{ timesince }} ago. 14 | {% endblocktrans %} 15 | {% else %} 16 | {{ notification }} 17 | {% endif %} 18 | {% elif notification.verb == 'edited' %} 19 | {% if notification.action_object|get_class == 'Hypothesis' %} 20 | {% blocktrans trimmed with notification.actor.id as actor_id and notification.actor.username as actor_username and notification.action_object as action_name and notification.target|board_url as custom_url and notification.target as target and notification.timesince as timesince %} 21 | {{ actor_username }} 22 | edited hypothesis '{{ action_name }}' on board {{ target }} {{ timesince }} ago. 23 | {% endblocktrans %} 24 | {% elif notification.action_object|get_class == 'Evidence' %} 25 | {% blocktrans trimmed with notification.actor.id as actor_id and notification.actor.username as actor_username and notification.action_object as action_name and notification.target|board_url as custom_url and notification.target as target and notification.timesince as timesince %} 26 | {{ actor_username }} 27 | edited evidence '{{ action_name }}' on board {{ target }} {{ timesince }} ago. 28 | {% endblocktrans %} 29 | {% else %} 30 | {{ notification }} 31 | {% endif %} 32 | {% else %} 33 | {{ notification }} 34 | {% endif %} 35 | -------------------------------------------------------------------------------- /openach/templates/boards/evaluate.html: -------------------------------------------------------------------------------- 1 | {% extends 'boards/base.html' %} 2 | {% load board_extras %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Evaluate Evidence" %} | {{ site.name }}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{% trans "Evaluate Evidence" %}

10 | 11 |

12 | {% trans "Instructions:" %} 13 | {% url 'openach:evidence_detail' evidence.id as evidence_url %} 14 | {% blocktrans trimmed %} 15 | Evaluate the consistency of each hypothesis with respect to the following evidence. We've hidden your previous 16 | evaluations and randomized the order of the hypotheses in order to prevent bias. If the evidence does not 17 | apply to a hypothesis, select N/A. 18 | {% endblocktrans %} 19 |

20 |

21 | {% blocktrans trimmed %} 22 | Assume that the evidence is valid. You can evaluate the validity of the evidence (and its sources) on 23 | the evidence detail page. 24 | {% endblocktrans %} 25 |

26 | 27 |
{{ evidence.evidence_desc }}
28 | 29 |
30 | {% csrf_token %} 31 | {% for hypothesis, eval_ in hypotheses %} 32 |
33 | 34 |
35 | 46 |
47 |
48 | {% endfor %} 49 |
50 |
51 | {% trans "Return to Board" %} 52 | 53 |
54 |
55 |
56 | 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Open Synthesis, an open platform for intelligence analysis 3 | * Copyright (C) 2016-2020 Open Synthesis Contributors. See CONTRIBUTING.md 4 | * file at the top-level directory of this distribution. 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | const path = require("path"); 20 | const webpack = require("webpack"); 21 | const BundleTracker = require("webpack-bundle-tracker"); 22 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 23 | 24 | module.exports = { 25 | context: __dirname, 26 | entry: path.resolve(__dirname, "openach-frontend/src/js/index.js"), 27 | output: { 28 | publicPath: "/static/", 29 | path: path.resolve("./openach-frontend/bundles/"), 30 | filename: "[name]-[hash].js", 31 | }, 32 | resolve: { 33 | alias: { 34 | css: path.resolve(__dirname, "openach-frontend/src/css/"), 35 | src: path.resolve(__dirname, "openach-frontend/src/js/"), 36 | }, 37 | }, 38 | plugins: [ 39 | new BundleTracker({ filename: "./webpack-stats.json" }), 40 | // https://webpack.github.io/docs/list-of-plugins.html#provideplugin 41 | new webpack.ProvidePlugin({ 42 | $: "jquery", 43 | jQuery: "jquery", 44 | }), 45 | ], 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.css$/, 50 | use: [ 51 | { 52 | loader: MiniCssExtractPlugin.loader, 53 | }, 54 | "css-loader", 55 | ], 56 | }, 57 | { 58 | test: /\.m?js$/, 59 | exclude: /(node_modules|bower_components)/, 60 | use: { 61 | loader: "babel-loader", 62 | options: { 63 | presets: ["@babel/preset-env"], 64 | }, 65 | }, 66 | }, 67 | { 68 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 69 | use: [ 70 | { 71 | loader: "file-loader", 72 | options: { 73 | name: "[name].[ext]", 74 | outputPath: "fonts/", 75 | }, 76 | }, 77 | ], 78 | }, 79 | ], 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /openintel/urls.py: -------------------------------------------------------------------------------- 1 | """Open Synthesis URL Configuration. 2 | 3 | See the Django documentation for more information: 4 | * https://docs.djangoproject.com/en/2.1/ref/urls/ 5 | * https://docs.djangoproject.com/en/2.1/topics/http/urls/ 6 | """ 7 | 8 | import notifications.urls 9 | from django.conf import settings 10 | from django.contrib import admin 11 | from django.contrib.sitemaps.views import sitemap 12 | from django.urls import include, path, re_path 13 | from django.views.generic import TemplateView 14 | 15 | from openach import views 16 | from openach.sitemap import BoardSitemap 17 | 18 | ACCOUNT_REQUIRED = getattr(settings, "ACCOUNT_REQUIRED", False) 19 | 20 | # NOTE: Django's API doesn't follow constant naming convention for 'sitemaps' and 'urlpatterns' 21 | 22 | # https://docs.djangoproject.com/en/1.10/ref/contrib/sitemaps/#initialization 23 | sitemaps = { # pylint: disable=invalid-name 24 | "board": BoardSitemap, 25 | } 26 | 27 | urlpatterns = [ # pylint: disable=invalid-name 28 | path("admin/", admin.site.urls), 29 | path("robots.txt", views.site.robots, name="robots"), 30 | path( 31 | "contribute.json", 32 | TemplateView.as_view( 33 | template_name="contribute.json", content_type="application/json" 34 | ), 35 | ), 36 | path( 37 | "accounts/signup/", 38 | views.site.CaptchaSignupView.as_view(), 39 | name="account_signup", 40 | ), 41 | path("accounts//", views.profiles.profile, name="profile"), 42 | path("accounts/profile/", views.profiles.private_profile, name="private_profile"), 43 | path("accounts/", include("allauth.urls")), 44 | path("comments/", include("django_comments.urls")), 45 | path( 46 | "inbox/notifications/", include(notifications.urls, namespace="notifications") 47 | ), 48 | path("invitations/", include("invitations.urls", namespace="invitations")), 49 | path("i18n/", include("django.conf.urls.i18n")), 50 | path("", include("openach.urls")), 51 | re_path( 52 | r"\.well-known/acme-challenge/(?P[a-zA-Z0-9\-_]+)$", 53 | views.site.certbot, 54 | ), 55 | ] 56 | 57 | 58 | if settings.DEBUG: 59 | import debug_toolbar 60 | 61 | urlpatterns = [ 62 | path("__debug__/", include(debug_toolbar.urls)), 63 | ] + urlpatterns 64 | 65 | 66 | if not ACCOUNT_REQUIRED: # pylint: disable=invalid-name 67 | # Only allow clients to view the sitemap if the admin is running a public instance 68 | urlpatterns.insert( 69 | 0, 70 | path( 71 | r"sitemap.xml", 72 | sitemap, 73 | {"sitemaps": sitemaps}, 74 | name="django.contrib.sitemaps.views.sitemap", 75 | ), 76 | ) # nopep8 77 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '40 19 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v6 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v4 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v4 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v4 72 | -------------------------------------------------------------------------------- /openach-frontend/src/js/notify.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Open Synthesis, an open platform for intelligence analysis 3 | * Copyright (C) 2016 Open Synthesis Contributors. See CONTRIBUTING.md 4 | * file at the top-level directory of this distribution. 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | // Adapted from: https://github.com/django-notifications/django-notifications/blob/master/notifications/static/notifications/notify.js 21 | 22 | // sessionStorage key for the last known number of unread notifications 23 | const NOTIFICATION_KEY = "unread_notifications"; 24 | const NOTIFY_REFRESH_PERIOD_MILLIS = 15 * 1000; 25 | const MAX_RETRIES = 5; 26 | 27 | /** 28 | * Returns a function that queries the number of unread notifications and sets the text of badge to the number. 29 | * 30 | * Re-queries the server every NOTIFY_REFRESH_PERIOD_MILLIS milliseconds. Stops querying the server if more than 31 | * MAX_RETRIES consecutive requests fail. 32 | * 33 | * @param badge the badge JQuery selector 34 | * @param {string} url the url to request 35 | * @returns {Function} function that updates the unread notification count 36 | */ 37 | function fetch_api_data(badge, url) { 38 | let consecutiveMisfires = 0; 39 | return function () { 40 | $.get(url, function (data) { 41 | consecutiveMisfires = 0; 42 | badge.text(data.unread_count); 43 | window.sessionStorage.setItem(NOTIFICATION_KEY, data.unread_count); 44 | }) 45 | .fail(function () { 46 | consecutiveMisfires++; 47 | }) 48 | .always(function () { 49 | if (consecutiveMisfires <= MAX_RETRIES) { 50 | setTimeout(fetch_api_data(badge, url), NOTIFY_REFRESH_PERIOD_MILLIS); 51 | } else { 52 | badge.text("!"); 53 | badge.prop("title", "No connection to server"); 54 | } 55 | }); 56 | }; 57 | } 58 | 59 | // NOTE: in practice, there will only be one element that has a data-notify-api-url attribute 60 | $("[data-notify-api-url]").each(function (index, badge) { 61 | const elt = $(badge); 62 | setTimeout(fetch_api_data(elt, elt.data("notify-api-url")), 1000); 63 | const previous = window.sessionStorage.getItem(NOTIFICATION_KEY); 64 | if (previous != null) { 65 | elt.text(previous); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Responsible Disclosure Policy 2 | 3 | Open Synthesis Responsible Disclosure Policy 4 | 5 | We take the security of our systems seriously, and we value the security community. The responsible disclosure of 6 | security vulnerabilities helps us ensure the security and privacy of our users. 7 | 8 | **Guidelines** 9 | 10 | We require that all researchers: 11 | * Make every effort to avoid privacy violations, degradation of user experience, disruption to production systems, and 12 | destruction of data during security testing; 13 | * Perform research only within the scope set out below; 14 | * Use the identified communication channels to report vulnerability information to us; and 15 | * Keep information about any vulnerabilities you’ve discovered confidential between yourself and Open Synthesis until 16 | we’ve had [60] days to resolve the issue. 17 | 18 | 19 | If you follow these guidelines when reporting an issue to us, we commit to: 20 | * Not pursue or support any legal action related to your research; 21 | * Work with you to understand and resolve the issue quickly (including an initial confirmation of your report within 22 | 72 hours of submission); 23 | * Recognize your contribution on our Security Researcher Hall of Fame, if you are the first to report the issue and we 24 | make a code or configuration change based on the issue. 25 | 26 | 27 | **Scope** 28 | * https://www.opensynthesis.org 29 | * https://open-synthesis-sandbox.herokuapp.com/ 30 | 31 | 32 | **Out of scope** 33 | Any services hosted by 3rd party providers and services are excluded from scope. These services include, but are not 34 | limited to: 35 | * GitHub 36 | * Gitter 37 | * Rollbar 38 | * SendGrid 39 | 40 | 41 | In the interest of the safety of our users, staff, the Internet at large and you as a security researcher, the 42 | following test types are excluded from scope: 43 | * Findings from physical testing such as office access (e.g. open doors, tailgating) 44 | * Findings derived primarily from social engineering (e.g. phishing, vishing) 45 | * Findings from applications or systems not listed in the 'Scope' section 46 | * UI and UX bugs and spelling mistakes 47 | * Network level Denial of Service (DoS/DDoS) vulnerabilities 48 | 49 | Things we do not want to receive: 50 | * Personally identifiable information (PII) 51 | * Credit card holder data 52 | 53 | 54 | **How to report a security vulnerability?** 55 | If you believe you’ve found a security vulnerability in one of our products or platforms please send it to us by 56 | emailing security@opensynthesis.org. Please include the following details with your report: 57 | 58 | * Description of the location and potential impact of the vulnerability; 59 | * A detailed description of the steps required to reproduce the vulnerability (POC scripts, screenshots, and compressed 60 | screen captures are all helpful to us); and 61 | * Your name/handle and a link for recognition in our Hall of Fame. 62 | 63 | If you’d like to encrypt the information, please use the following PGP key: https://keybase.io/tschiller. 64 | -------------------------------------------------------------------------------- /openach/templates/teams/_member_panel.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
4 |
5 |
6 | {% if team.owner != request.user %} 7 | {% if not is_member %} 8 |
9 | {% csrf_token %} 10 | {% if team.members.all|length > 10 %} 11 | {% trans "View All" %} 12 | {% endif %} 13 | {% if pending_request %} 14 | 15 | {% elif pending_invitation %} 16 | 17 | {% elif team.invitation_required %} 18 | 19 | {% else %} 20 | 21 | {% endif %} 22 |
23 | {% else %} 24 |
25 | {% csrf_token %} 26 | {% if team.members.all|length > 10 %} 27 | {% trans "View All" %} 28 | {% endif %} 29 | 30 |
31 | {% endif %} 32 | {% endif %} 33 |
34 |

{% trans "Team Members" %}

35 |
36 |
    37 | {% for member in team.members.all|slice:":10" %} 38 |
  • 39 | {% if member.id != team.owner.id and request.user == team.owner %} 40 |
    41 | {% csrf_token %} 42 | {{ member.username }} 43 | 44 |
    45 | {% else %} 46 | {{ member.username }} 47 | {% endif %} 48 |
  • 49 | {% empty %} 50 |
  • {% trans "Team has no members" %}
  • 51 | {% endfor %} 52 |
53 |
54 | -------------------------------------------------------------------------------- /openach/views/profiles.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.contrib import messages 5 | from django.contrib.auth.decorators import login_required 6 | from django.contrib.auth.models import User 7 | from django.shortcuts import get_object_or_404, render 8 | from django.utils.translation import gettext as _ 9 | from django.views.decorators.cache import cache_page 10 | from django.views.decorators.http import require_http_methods, require_safe 11 | 12 | from openach.decorators import account_required 13 | from openach.forms import SettingsForm 14 | from openach.metrics import ( 15 | user_boards_contributed, 16 | user_boards_created, 17 | user_boards_evaluated, 18 | ) 19 | 20 | PAGE_CACHE_TIMEOUT_SECONDS = getattr(settings, "PAGE_CACHE_TIMEOUT_SECONDS", 60) 21 | 22 | logger = logging.getLogger(__name__) # pylint: disable=invalid-name 23 | 24 | 25 | @require_http_methods(["HEAD", "GET", "POST"]) 26 | @login_required 27 | def private_profile(request): 28 | """Return a view of the private profile associated with the authenticated user and handle settings.""" 29 | user = request.user 30 | 31 | if request.method == "POST": 32 | form = SettingsForm(request.POST, instance=user.settings) 33 | if form.is_valid(): 34 | form.save() 35 | messages.success(request, _("Updated account settings.")) 36 | else: 37 | form = SettingsForm(instance=user.settings) 38 | 39 | context = { 40 | "user": user, 41 | "boards_created": user_boards_created(user, viewing_user=user)[:5], 42 | "boards_contributed": user_boards_contributed(user, viewing_user=user), 43 | "board_voted": user_boards_evaluated(user, viewing_user=user), 44 | "meta_description": _("Account profile for user {name}").format(name=user), 45 | "notifications": request.user.notifications.unread(), 46 | "settings_form": form, 47 | } 48 | return render(request, "boards/profile.html", context) 49 | 50 | 51 | @require_safe 52 | @cache_page(PAGE_CACHE_TIMEOUT_SECONDS) 53 | def public_profile(request, account_id): 54 | """Return a view of the public profile associated with account_id.""" 55 | user = get_object_or_404(User, pk=account_id) 56 | context = { 57 | "user": user, 58 | "boards_created": user_boards_created(user, viewing_user=request.user)[:5], 59 | "boards_contributed": user_boards_contributed(user, viewing_user=request.user), 60 | "board_voted": user_boards_evaluated(user, viewing_user=request.user), 61 | "meta_description": _("Account profile for user {name}").format(name=user), 62 | } 63 | return render(request, "boards/public_profile.html", context) 64 | 65 | 66 | @require_http_methods(["HEAD", "GET", "POST"]) 67 | @account_required 68 | def profile(request, account_id): 69 | """Return a view of the profile associated with account_id. 70 | 71 | If account_id corresponds to the authenticated user, return the private profile view. Otherwise return the public 72 | profile. 73 | """ 74 | return ( 75 | private_profile(request) 76 | if request.user.id == int(account_id) 77 | else public_profile(request, account_id) 78 | ) 79 | -------------------------------------------------------------------------------- /openach/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | from django.utils import timezone 6 | 7 | from openach.models import Board 8 | from openach.tests.common import PrimaryUserTestCase, remove 9 | 10 | 11 | class RemovableModelManagerTests(TestCase): 12 | def test_objects_does_not_include_removed(self): 13 | """Test that after an object is marked as removed, it doesn't appear in the query set.""" 14 | board = Board.objects.create( 15 | board_title="Title", board_desc="Description", pub_date=timezone.now() 16 | ) 17 | self.assertEqual(Board.objects.count(), 1) 18 | remove(board) 19 | self.assertEqual(Board.objects.count(), 0) 20 | self.assertEqual(Board.all_objects.count(), 1) 21 | 22 | 23 | class BoardMethodTests(TestCase): 24 | def test_was_published_recently_with_future_board(self): 25 | """Test that was_published_recently() returns False for board whose pub_date is in the future.""" 26 | time = timezone.now() + datetime.timedelta(days=30) 27 | future_board = Board(pub_date=time) 28 | self.assertIs(future_board.was_published_recently(), False) 29 | 30 | def test_was_published_recently_with_old_question(self): 31 | """Test that was_published_recently() returns False for boards whose pub_date is older than 1 day.""" 32 | time = timezone.now() - datetime.timedelta(days=30) 33 | old_board = Board(pub_date=time) 34 | self.assertIs(old_board.was_published_recently(), False) 35 | 36 | def test_was_published_recently_with_recent_question(self): 37 | """Test that was_published_recently() returns True for boards whose pub_date is within the last day.""" 38 | time = timezone.now() - datetime.timedelta(hours=1) 39 | recent_board = Board(pub_date=time) 40 | self.assertIs(recent_board.was_published_recently(), True) 41 | 42 | def test_board_url_without_slug(self): 43 | """Test to make sure we can grab the URL of a board that has no slug.""" 44 | self.assertIsNotNone(Board(id=1).get_absolute_url()) 45 | 46 | def test_board_url_with_slug(self): 47 | """Test to make sure we can grab the URL of a board that has a slug.""" 48 | slug = "test-slug" 49 | self.assertTrue(slug in Board(id=1, board_slug=slug).get_absolute_url()) 50 | 51 | 52 | class BoardTitleTests(PrimaryUserTestCase): 53 | def test_board_title_special_characters(self): 54 | """Test to make sure certain characters are allowed/disallowed in titles.""" 55 | board = Board( 56 | board_desc="Test Board Description", 57 | creator=self.user, 58 | pub_date=timezone.now(), 59 | ) 60 | fail_titles = [ 61 | "Test Board Title!", 62 | "Test Board @ Title", 63 | "Test #Board Title", 64 | "Test Board Title++++", 65 | ] 66 | for title in fail_titles: 67 | board.board_title = title 68 | self.assertRaises(ValidationError, board.full_clean) 69 | 70 | pass_titles = [ 71 | "Test Böard Titlé", 72 | "Test (Board) Title?", 73 | "Test Board & Title", 74 | "Test Board/Title", 75 | ] 76 | for title in pass_titles: 77 | board.board_title = title 78 | board.full_clean() 79 | -------------------------------------------------------------------------------- /openach/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.conf import settings 4 | from django.contrib.auth.models import User 5 | from django.core import mail 6 | from django.test import TestCase, override_settings 7 | 8 | DEFAULT_FROM_EMAIL = getattr(settings, "DEFAULT_FROM_EMAIL", "admin@localhost") 9 | 10 | 11 | class AccountManagementTests(TestCase): 12 | """Project-specific account management tests. General tests should be in the django-allauth library.""" 13 | 14 | username = "testuser" 15 | 16 | email = "testemail@google.com" 17 | 18 | valid_data = { 19 | "username": username, 20 | "email": email, 21 | "password1": "testpassword1!", 22 | "password2": "testpassword1!", 23 | } 24 | 25 | @override_settings( 26 | INVITATIONS_INVITATION_ONLY=False, INVITE_REQUEST_URL="https://google.com" 27 | ) 28 | def test_can_show_signup_form(self): 29 | """Test that a non-logged-in user can view the sign-up form.""" 30 | response = self.client.get("/accounts/signup/") 31 | self.assertTemplateUsed("/account/email/signup.html") 32 | self.assertNotContains(response, "invitation") 33 | 34 | @override_settings( 35 | INVITATIONS_INVITATION_ONLY=True, INVITE_REQUEST_URL="https://google.com" 36 | ) 37 | def test_can_show_invite_url(self): 38 | """Test that a non-logged-in user can view the sign-up form that has an invite link.""" 39 | response = self.client.get("/accounts/signup/") 40 | self.assertContains(response, "invitation") 41 | 42 | @override_settings(ACCOUNT_EMAIL_REQUIRED=True) 43 | def test_email_address_required(self): 44 | """Test that signup without email is rejected.""" 45 | response = self.client.post( 46 | "/accounts/signup/", data={**self.valid_data, "email": ""} 47 | ) 48 | self.assertTemplateUsed("account/signup.html") 49 | # behavior is for the form-group of the email address to have has-error and to have its help text set 50 | self.assertContains(response, "This field is required.") 51 | 52 | @override_settings( 53 | ACCOUNT_EMAIL_REQUIRED=True, ACCOUNT_EMAIL_VERIFICATION="mandatory" 54 | ) 55 | @patch("allauth.account.signals.email_confirmation_sent.send") 56 | def test_account_signup_flow(self, mock): 57 | """Test that the user receives a confirmation email when they signup for an account with an email address.""" 58 | response = self.client.post("/accounts/signup/", data=self.valid_data) 59 | self.assertRedirects(response, expected_url="/accounts/confirm-email/") 60 | 61 | self.assertEqual(mock.call_count, 1) 62 | self.assertEqual(len(mail.outbox), 1, "No confirmation email sent") 63 | 64 | # The example.com domain comes from django.contrib.sites plugin 65 | self.assertEqual( 66 | mail.outbox[0].subject, "[example.com] Please Confirm Your E-mail Address" 67 | ) 68 | self.assertListEqual(mail.outbox[0].to, [self.email]) 69 | self.assertEqual(mail.outbox[0].from_email, DEFAULT_FROM_EMAIL) 70 | 71 | @override_settings(ACCOUNT_EMAIL_REQUIRED=False) 72 | def test_settings_created(self): 73 | """Test that a settings object is created when the user is created.""" 74 | self.client.post("/accounts/signup/", data=self.valid_data) 75 | user = User.objects.get(username=self.username) 76 | self.assertIsNotNone(user.settings, "User settings object not created") 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Synthesis 2 | 3 | [![Build Status](https://travis-ci.org/twschiller/open-synthesis.svg?branch=master)](https://travis-ci.org/twschiller/open-synthesis) 4 | [![Coverage Status](https://coveralls.io/repos/github/twschiller/open-synthesis/badge.svg?branch=master)](https://coveralls.io/github/twschiller/open-synthesis?branch=master) 5 | [![Code Climate Status](https://codeclimate.com/github/twschiller/open-synthesis/badges/gpa.svg)](https://codeclimate.com/github/twschiller/open-synthesis) 6 | [![License Status](https://img.shields.io/badge/license-AGPL-brightgreen.svg)](LICENSE.md) 7 | 8 | ## 2024-03-09: THIS REPOSITORY IS IN MAINTENANCE MODE 9 | 10 | This repository is in maintenance mode. I will be applying security dependency updates, but am not accepting issues/PRs. 11 | 12 | ## Goals 13 | 14 | The purpose of the Open Synthesis project is to empower the public to synthesize vast amounts of information into actionable conclusions. 15 | 16 | To this end, the platform and its governance aims to be: 17 | 18 | 1. Inclusive 19 | 2. Transparent 20 | 3. Meritocratic 21 | 22 | Our approach is to take best practices from the intelligence and business communities and adapt them to work with internet 23 | communities. 24 | 25 | ## Analysis of Competing Hypotheses (ACH) 26 | 27 | Initially, the platform will support the [Analysis of Competing Hypotheses (ACH) framework.](https://en.wikipedia.org/wiki/Analysis_of_competing_hypotheses) 28 | ACH was developed by Richards J. Heuer, Jr. for use at the United States Central Intelligence Agency (CIA). 29 | 30 | The ACH framework is a good candidate for public discourse because: 31 | 32 | * ACH's hypothesis generation and evidence cataloging benefit from a diversity of perspectives 33 | * ACH's process for combining the viewpoints of participants is straightforward and robust 34 | * ACH can account for unreliable evidence, e.g., due to deception 35 | 36 | The initial implementation will be similar to [competinghypotheses.org](http://competinghypotheses.org/). However, we 37 | will adapt the implementation to address the challenges of public discourse. 38 | 39 | ## Platform Design Principles 40 | 41 | The platform will host the analysis of politically sensitive topics. Therefore, its design must strike a balance between 42 | freedom of speech, safety, and productivity. More specific concerns include: 43 | 44 | * Open-Source Licensing and Governance 45 | * Privacy 46 | * Accessibility 47 | * Internationalization and Localization 48 | * Moderation vs. Censorship 49 | 50 | ## Deploying to Heroku 51 | 52 | Detailed instructions for deploying your own instance can be found on the 53 | [Custom Deployments wiki page](https://github.com/twschiller/open-synthesis/wiki/Custom-Deployments). 54 | 55 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 56 | 57 | ## Copyright 58 | 59 | Copyright (C) 2016-2023 Open Synthesis Contributors. See CONTRIBUTING.md file at the top-level directory of this 60 | distribution. 61 | 62 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General 63 | Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) 64 | any later version. 65 | 66 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 67 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 68 | more details. 69 | 70 | You should have received a copy of the GNU Affero General Public License along with this program. 71 | If not, see . 72 | --------------------------------------------------------------------------------