diff --git a/euth/actions/admin.py b/euth/actions/admin.py deleted file mode 100644 index 96c15fdf9..000000000 --- a/euth/actions/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin - -from .models import Action - -admin.site.register(Action) diff --git a/euth/actions/migrations/0004_delete_action.py b/euth/actions/migrations/0004_delete_action.py new file mode 100644 index 000000000..58bc08e00 --- /dev/null +++ b/euth/actions/migrations/0004_delete_action.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.20 on 2024-09-09 13:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_actions', '0003_restrict_verbs_by_choices'), + ] + + operations = [ + migrations.DeleteModel( + name='Action', + ), + ] diff --git a/euth/actions/models.py b/euth/actions/models.py index b36dd115a..e69de29bb 100644 --- a/euth/actions/models.py +++ b/euth/actions/models.py @@ -1,68 +0,0 @@ -from django.conf import settings -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.db import models -from django.utils import timezone - -from adhocracy4.projects.models import Project - -from . import verbs - - -class Action(models.Model): - - # actor, if actor is None the action was create by the system - actor = models.ForeignKey(settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - blank=True, - null=True) - - # target eg. idea - target_content_type = models.ForeignKey(ContentType, - blank=True, - null=True, - on_delete=models.CASCADE, - related_name='target') - target_object_id = models.CharField(max_length=255, blank=True, null=True) - target = GenericForeignKey( - ct_field='target_content_type', fk_field='target_object_id') - - # action object eg. comment - action_object_content_type = models.ForeignKey( - ContentType, - blank=True, - null=True, - on_delete=models.CASCADE, - related_name='action_object') - action_object_object_id = models.CharField( - max_length=255, blank=True, null=True) - action_object = GenericForeignKey( - ct_field='action_object_content_type', - fk_field='action_object_object_id') - - # project - project = models.ForeignKey( - Project, on_delete=models.CASCADE, blank=True, null=True) - - timestamp = models.DateTimeField(default=timezone.now) - public = models.BooleanField(default=True, db_index=True) - verb = models.CharField(max_length=255, db_index=True, choices=verbs.all()) - description = models.TextField(blank=True, null=True) - - def __str__(self): - - ctx = { - 'actor': self.actor.username if self.actor else 'system', - 'verb': self.verb, - 'action_object': self.action_object, - 'target': self.target - } - - if self.target: - if self.action_object: - return '{actor} {verb} {action_object} on {target}'.format( - **ctx) - return '{actor} {verb} {target}'.format(**ctx) - if self.action_object: - return '{actor} {verb} {action_object}'.format(**ctx) - return '{actor} {verb}'.format(**ctx) diff --git a/euth/actions/signals.py b/euth/actions/signals.py index 4326a6b31..e69de29bb 100644 --- a/euth/actions/signals.py +++ b/euth/actions/signals.py @@ -1,49 +0,0 @@ -from django.apps import apps -from django.conf import settings -from django.db.models.signals import post_save -from django.dispatch import receiver - -from adhocracy4.projects.models import Project -from euth.actions import emails - -from . import verbs -from .models import Action - - -def add_action(sender, instance, created, **kwargs): - verb = verbs.CREATE if created else verbs.UPDATE - actor = instance.creator - - action = Action( - actor=actor, - verb=verb, - action_object=instance - ) - - if hasattr(instance, 'project') and instance.project.__class__ is Project: - action.project = instance.project - action.target = instance.project - - if hasattr(instance, 'content_object'): - action.target = instance.content_object - - action.save() - - -for app, model in settings.ACTIONABLE: - post_save.connect(add_action, apps.get_model(app, model)) - - -@receiver(post_save, sender=Action) -def send_notification(sender, instance, created, **kwargs): - action = instance - - if instance.verb == verbs.CREATE: - emails.NotifyCreatorEmail.send(action) - - if action.target_content_type.model_class() is Project: - emails.NotifyModeratorsEmail.send(action) - emails.NotifyFollowersOnNewIdeaCreated.send(action) - - if instance.verb == verbs.COMPLETE: - emails.NotifyFollowersOnPhaseIsOverSoonEmail.send(action) diff --git a/euth/blueprints/__init__.py b/euth/blueprints/__init__.py deleted file mode 100644 index 70a416075..000000000 --- a/euth/blueprints/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'euth.blueprints.apps.Config' diff --git a/euth/blueprints/apps.py b/euth/blueprints/apps.py deleted file mode 100644 index 0bb1a18d0..000000000 --- a/euth/blueprints/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class Config(AppConfig): - name = 'euth.blueprints' - label = 'euth_blueprints' diff --git a/euth/blueprints/blueprints.py b/euth/blueprints/blueprints.py deleted file mode 100644 index 22429f65e..000000000 --- a/euth/blueprints/blueprints.py +++ /dev/null @@ -1,362 +0,0 @@ -from collections import namedtuple -from enum import Enum -from enum import unique - -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.polls import phases as poll_phases -from euth.communitydebate import phases as communitydebate_phases -from euth.documents import phases as documents_phases -from euth.ideas import phases as ideas_phases -from euth.maps import phases as map_phases - -from .names import BlueprintNames - - -class BlueprintEnum(Enum): - def __new__(cls, value, label): - obj = object.__new__(cls) - obj._value_ = len(cls.__members__) + 1 - obj._value = value - obj.label = label - return obj - - @property - def value(self): - return self._value - - @classmethod - def get(cls, value): - return next(m for m in cls if m._value == value) - - -@unique -class Aim(Enum): - collect_ideas = ( - 'collect_ideas', - _('Create and collect new ideas or visions.'), - [_('(Urban) planning processes'), - _('Develop concepts or guiding principles')] - ) - discuss_topic = ( - 'discuss_topic', - _('Gather feedback on a topic and discuss it in greater detail.'), - [_('Discuss existing concepts or plans'), - _('Develop solutions for existing problems')] - ) - agenda_setting = ( - 'agenda_setting', - _('Agenda Setting'), - [_('Set the agenda of an event, a process, a project etc.')] - ) - design_place = ( - 'design_place', - _('Design a place.'), - [_('(Urban) planning processes'), - _('Small scale design projects, e.g. renew your premises')] - ) - run_survey = ( - 'run_survey', - _('Learn about what people like most.'), - [_('Opinion polls, majority votes etc.')] - ) - run_competition = ( - 'run_competition', - _('Run a competition.'), - [_('All sorts of competitions, ' - 'like idea contests etc.')] - ) - work_document = ( - 'work_document', - _('Work together with other people on a text document.'), - [_('Draft or revise statutes, articles, charters etc.'), - _('Involve different authors in writing a shared text')] - ) - communitydebate = ( - 'communitydebate', - _('Find and debate topics and questions.'), - [_('Start a discussion and moderate it'), - _('Participants can upload documents to deepen the exchange ' - 'and share information')] - ) - - def __new__(cls, value, label, examples): - obj = object.__new__(cls) - obj._value_ = value - obj.label = label - obj.examples = examples - return obj - - -@unique -class Result(BlueprintEnum): - collect_ideas = 3, _('Collection of commented ideas') - majority_vote = 2, _('Majority vote') - both = 1, _('Both') - - -@unique -class Experience(BlueprintEnum): - five_projects = 4, _('More than 5 participative projects') - two_projects = 3, _('More than 2 participative projects') - one_project = 2, _('1-2 participative projects') - no_projects = 1, _('I have no experiences in organising ' - 'participative projects') - - -class Motivation(BlueprintEnum): - high = 4, _('High motivation') - medium = 3, _('Medium motivation') - low = 2, _('Low motivation') - not_found = 1, _('No motivation') - unkown = 2, _('I don\'t know.') - - -@unique -class Participants(BlueprintEnum): - few = 0, '< 25' - some = 1, '25-50' - many = 2, '50+' - - -@unique -class Duration(BlueprintEnum): - one_weeks = 0, _('1-2 weeks') - two_weeks = 1, _('2-4 weeks') - four_weeks = 2, _('more than 4 weeks') - - -@unique -class Scope(BlueprintEnum): - local = 0, _('Local') - regional = 1, _('Regional') - national = 2, _('National or international') - - -class Accessibility(BlueprintEnum): - very_easy = 1, _('Very easy to access') - easy = 2, _('Easy to access') - hard = 3, _('Hard to access') - very_hard = 4, _('Very hard to access') - unkown = 3, _('I don\'t know') - - -ComplexityVector = namedtuple( - 'ComplexityVector', [ - 'participants', 'duration', 'scope' - ] -) - - -COMPLEXITY_VECTOR_AC = ComplexityVector( - participants=(0, 0.5), - duration=(0, 1), - scope=(0, 0.5) -) - -COMPLEXITY_VECTOR_BD = ComplexityVector( - participants=(0, 1), - duration=(0, 1), - scope=(0, 1) -) - -COMPLEXITY_VECTOR_E = ComplexityVector( - participants=(0, 1 / 3), - duration=(0, 0), - scope=(0, 1 / 3) -) - -COMPLEXITY_VECTOR_F = ComplexityVector( - participants=(1, 1), - duration=(0, 1), - scope=(0, 0) -) - -Requirements = namedtuple( - 'Requirements', [ - 'aims', 'results', 'experience', 'motivation' - ]) - - -Blueprint = namedtuple( - 'Blueprint', [ - 'title', 'description', 'content', 'image', 'settings_model', - 'requirements', 'complexity', 'type' - ]) - - -blueprints = [ - (BlueprintNames.brainstorming.value, - Blueprint( - title=_('Brainstorming'), - description=_('Collect ideas, questions and input concerning ' - 'a problem or a question from a wide array of people.'), - content=[ - ideas_phases.CollectPhase(), - ], - image='images/brainstorming.png', - settings_model=None, - requirements=Requirements( - aims=[Aim.collect_ideas, Aim.discuss_topic], - results=[Result.collect_ideas], - experience=Experience.no_projects, - motivation=Motivation.not_found - ), - complexity=COMPLEXITY_VECTOR_AC, - type=BlueprintNames.brainstorming.name - )), - (BlueprintNames.map_brainstorming.value, - Blueprint( - title=_('Spatial Brainstorming'), - description=_('Collect ideas, questions and input concerning a ' - 'problem or a question from a wide array of people.'), - content=[ - map_phases.CollectPhase(), - ], - image='images/spatial_brainstorming.png', - settings_model=('a4maps', 'AreaSettings'), - requirements=Requirements( - aims=[Aim.design_place], - results=[Result.collect_ideas], - experience=Experience.no_projects, - motivation=Motivation.not_found - ), - complexity=COMPLEXITY_VECTOR_AC, - type=BlueprintNames.map_brainstorming.name - )), - (BlueprintNames.idea_challenge.value, - Blueprint( - title=_('Idea Challenge'), - description=_('Run a challenge and find the best ideas to solve ' - 'a particular problem.'), - content=[ - ideas_phases.CollectPhase(), - ideas_phases.RatingPhase(), - ], - image='images/challenge.png', - settings_model=None, - requirements=Requirements( - aims=[Aim.run_competition, Aim.run_survey], - results=list(Result), - experience=Experience.one_project, - motivation=Motivation.low - ), - complexity=COMPLEXITY_VECTOR_BD, - type=BlueprintNames.idea_challenge.name - )), - (BlueprintNames.map_idea_challenge.value, - Blueprint( - title=_('Spatial Idea Challenge'), - description=_('Run a challenge concerning a certain area or space in ' - 'your community and find the best ideas to solve a ' - 'particular problem.'), - content=[ - map_phases.CollectPhase(), - map_phases.RatingPhase(), - ], - image='images/spatial_challenge.png', - settings_model=('a4maps', 'AreaSettings'), - requirements=Requirements( - aims=[Aim.design_place], - results=list(Result), - experience=Experience.one_project, - motivation=Motivation.low - ), - complexity=COMPLEXITY_VECTOR_BD, - type=BlueprintNames.map_idea_challenge.name - )), - (BlueprintNames.agenda_setting.value, - Blueprint( - title=_('Agenda Setting'), - description=_('You can involve everyone in planning a meeting. ' - 'Collect ideas for an upcoming event and let your ' - 'participants vote on the topics you want to tackle.'), - content=[ - ideas_phases.CollectPhase(), - ideas_phases.RatingPhase(), - ], - image='images/agenda_setting.png', - settings_model=None, - requirements=Requirements( - aims=[Aim.collect_ideas, Aim.discuss_topic, - Aim.run_survey, Aim.agenda_setting], - results=list(Result), - experience=Experience.one_project, - motivation=Motivation.low - ), - complexity=COMPLEXITY_VECTOR_AC, - type=BlueprintNames.agenda_setting.name - )), - (BlueprintNames.commenting_text.value, - Blueprint( - title=_('Text Review'), - description=_('Let participants discuss individual paragraphs of a ' - 'text. This is ideal for discussing position papers or ' - 'a mission statements with many people.'), - content=[ - documents_phases.CreateDocumentPhase(), - documents_phases.CommentPhase(), - ], - image='images/text_review.png', - settings_model=None, - requirements=Requirements( - aims=[Aim.work_document], - results=None, - experience=None, - motivation=None - ), - complexity=COMPLEXITY_VECTOR_F, - type=BlueprintNames.commenting_text.name - )), - (BlueprintNames.a4_poll.value, - Blueprint( - title=_('Poll'), - description=_('Run customizable, multi-step polls on OPIN to get ' - 'detailed opinions on topics from the public or your ' - 'members.'), - content=[ - poll_phases.VotingPhase(), - ], - image='images/poll.png', - settings_model=None, - requirements=Requirements( - aims=[Aim.run_survey], - results=[Result.majority_vote], - experience=Experience.no_projects, - motivation=Motivation.not_found - ), - complexity=COMPLEXITY_VECTOR_E, - type=BlueprintNames.a4_poll.name - )), - (BlueprintNames.communitydebate.value, - Blueprint( - title=_('Community debate'), - description=_('Collect topics and questions to discuss, ' - 'debate and prioritize them.'), - content=[ - communitydebate_phases.DebatePhase(), - ], - image='images/brainstorming.png', - settings_model=None, - requirements=Requirements( - aims=[Aim.communitydebate], - results=[Result.both], - experience=Experience.no_projects, - motivation=Motivation.not_found - ), - complexity=COMPLEXITY_VECTOR_AC, - type=BlueprintNames.communitydebate.name - )), -] - - -fallbacks = { - Aim.collect_ideas: BlueprintNames.brainstorming.value, - Aim.discuss_topic: BlueprintNames.brainstorming.value, - Aim.agenda_setting: BlueprintNames.agenda_setting.value, - Aim.design_place: BlueprintNames.map_brainstorming.value, - Aim.run_survey: BlueprintNames.a4_poll.value, - Aim.run_competition: BlueprintNames.agenda_setting.value, - Aim.work_document: BlueprintNames.commenting_text.value, - Aim.communitydebate: BlueprintNames.communitydebate.value -} diff --git a/euth/blueprints/forms.py b/euth/blueprints/forms.py deleted file mode 100644 index 2f5990937..000000000 --- a/euth/blueprints/forms.py +++ /dev/null @@ -1,103 +0,0 @@ -from django import forms -from django.core.exceptions import ValidationError -from django.utils.translation import gettext_lazy as _ - -from . import blueprints - - -class GetSuggestionForm(forms.Form): - aim_choices = [ - {'value': a.value, 'label': a.label, 'example': a.examples} - for a in blueprints.Aim - ] - - aim = forms.ChoiceField( - choices=[(aim.value, aim.label) for aim in blueprints.Aim], - required=True - ) - - result = forms.ChoiceField( - choices=[(r.value, r.label) for r in blueprints.Result], - widget=forms.RadioSelect, - required=True, - label=_('What is the desired outcome of the project?'), - ) - - experience = forms.ChoiceField( - choices=[(e.value, e.label) for e in blueprints.Experience], - widget=forms.RadioSelect, - required=True, - label=_('How many on- and offline participative projects ' - 'have you organised and managed in the past?') - ) - - motivation = forms.ChoiceField( - choices=[(m.value, m.label) for m in blueprints.Motivation], - required=True, - widget=forms.RadioSelect, - label=_('How motivated are your participants to take part in a ' - ' participative process?') - ) - - participants = forms.ChoiceField( - choices=[(m.value, m.label) for m in blueprints.Participants], - required=True, - widget=forms.RadioSelect, - label=_('How many participants might take part in the project?') - ) - - scope = forms.ChoiceField( - choices=[(m.value, m.label) for m in blueprints.Scope], - required=True, - widget=forms.RadioSelect, - label=_('What is the scope of the project?') - ) - - duration = forms.ChoiceField( - choices=[(m.value, m.label) for m in blueprints.Duration], - required=True, - widget=forms.RadioSelect, - label=_('How long do you want your project to be?') - ) - - accessibility = forms.ChoiceField( - choices=[(m.value, m.label) for m in blueprints.Accessibility], - required=True, - widget=forms.RadioSelect, - label=_('How easy can you access your participants?') - ) - - def clean_aim(self, *args, **kwargs): - try: - return blueprints.Aim(self.cleaned_data['aim']) - except KeyError: - raise ValidationError(_('Invalid aim selected')) - - def _clean_enum(self, name, enum): - try: - return enum.get(int(self.cleaned_data[name])) - except (KeyError, ValueError): - raise ValidationError(_('Invalid selection')) - - def clean_result(self, *args, **kwargs): - return self._clean_enum('result', blueprints.Result) - - def clean_experience(self, *args, **kwargs): - return self._clean_enum('experience', blueprints.Experience) - - def clean_motivation(self, *args, **kwargs): - return self._clean_enum('motivation', blueprints.Motivation) - - def clean_participants(self, *args, **kwargs): - return self._clean_enum( - 'participants', blueprints.Participants) - - def clean_scope(self, *args, **kwargs): - return self._clean_enum('scope', blueprints.Scope) - - def clean_duration(self, *args, **kwargs): - return self._clean_enum('duration', blueprints.Duration) - - def clean_accessibility(self, *args, **kwargs): - return self._clean_enum( - 'accessibility', blueprints.Accessibility) diff --git a/euth/blueprints/migrations/__init__.py b/euth/blueprints/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/euth/blueprints/mixins.py b/euth/blueprints/mixins.py deleted file mode 100644 index fcd8d969e..000000000 --- a/euth/blueprints/mixins.py +++ /dev/null @@ -1,7 +0,0 @@ -from .blueprints import blueprints - - -class BlueprintMixin(): - @property - def blueprint(self): - return dict(blueprints)[self.kwargs['blueprint_slug']] diff --git a/euth/blueprints/names.py b/euth/blueprints/names.py deleted file mode 100644 index b14e1ef69..000000000 --- a/euth/blueprints/names.py +++ /dev/null @@ -1,12 +0,0 @@ -from enum import Enum - - -class BlueprintNames(Enum): - brainstorming = 'brainstorming' - map_brainstorming = 'map-brainstorming' - idea_challenge = 'idea-challenge' - map_idea_challenge = 'map-idea-challenge' - agenda_setting = 'agenda-setting' - commenting_text = 'commenting-text' - a4_poll = 'a4-poll' - communitydebate = 'communitydebate' diff --git a/euth/blueprints/static/euth_blueprintsuggest/js/blueprintsuggest.js b/euth/blueprints/static/euth_blueprintsuggest/js/blueprintsuggest.js deleted file mode 100644 index b2d0283b5..000000000 --- a/euth/blueprints/static/euth_blueprintsuggest/js/blueprintsuggest.js +++ /dev/null @@ -1,89 +0,0 @@ -/* global django */ -(function ($) { - const blueprintsuggest = { - init: function () { - $('.js-continue').on('click', this.clickContinueHandler.bind(this)) - $('.js-back').on('click', this.clickBackHandler) - $('.js-send').on('click', this.clickSendHandler.bind(this)) - }, - - validate: function ($tab) { - /* - * Ensure that for each group of radio buttons in $tab at least one is checked. - */ - const $radioButtons = $tab.find('input[type=radio]') - const radioButtonsByName = {} - $radioButtons.map(function () { - const $this = $(this) - const name = $this.attr('name') - - if (!Object.prototype.hasOwnProperty.call(radioButtonsByName, name)) { - radioButtonsByName[name] = $() - } - radioButtonsByName[name] = radioButtonsByName[name].add($this) - - return radioButtonsByName[name] - }) - for (const key in radioButtonsByName) { - if (Object.prototype.hasOwnProperty.call(radioButtonsByName, key)) { - const $inputs = radioButtonsByName[key] - if (!$inputs.filter(':checked').length) { - return false - } - } - } - - return true - }, - - clickContinueHandler: function (e) { - const $this = $(e.target) - const $tab = $this.parents('.tab-pane') - const isValid = this.validate($tab) - - // remove old errorlist - $tab.find('.errorlist').remove() - - if (!isValid) { - // there's no radio button checked, not valid, so add new errorlist - $tab.find('.dst-lightbox-progress').before(this.getErrorElement()) - return true - } - - $tab.removeClass('active').next().addClass('active') - return false - }, - - clickSendHandler: function (e) { - const $this = $(e.target) - const $tab = $this.parents('.tab-pane') - const isValid = this.validate($tab) - - // remove old errorlist - $tab.find('.errorlist').remove() - - if (!isValid) { - // there is some radio button not checked, not valid, so add new errorlist - $tab.find('.dst-lightbox-progress').before(this.getErrorElement()) - e.preventDefault() - return false - } - - return true - }, - - getErrorElement: function () { - const text = django.gettext('Please set all values for your project.') - return '' - }, - - clickBackHandler: function () { - const $tab = $(this).parents('.tab-pane') - $tab.removeClass('active').prev().addClass('active') - } - } - - $(function () { - blueprintsuggest.init() - }) -}(jQuery)) diff --git a/euth/blueprints/templates/euth_blueprints/form.html b/euth/blueprints/templates/euth_blueprints/form.html deleted file mode 100644 index 3027a16f4..000000000 --- a/euth/blueprints/templates/euth_blueprints/form.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'base.html' %} -{% load i18n static %} - -{% block title %}{% trans 'Find the right project template' %}{% endblock %} - -{% block extra_js %} - {{ block.super }} - -{% endblock %} - -{% block content %} -
-
- {% csrf_token %} - - -
- -
- {% include 'euth_blueprints/includes/step-aim.html' %} - {% include 'euth_blueprints/includes/step-finetuning.html' %} - {% include 'euth_blueprints/includes/step-feasibility.html' %} -
-
-
-
-{% endblock %} diff --git a/euth/blueprints/templates/euth_blueprints/includes/breadcrumbs.html b/euth/blueprints/templates/euth_blueprints/includes/breadcrumbs.html deleted file mode 100644 index 8aacf15cd..000000000 --- a/euth/blueprints/templates/euth_blueprints/includes/breadcrumbs.html +++ /dev/null @@ -1,10 +0,0 @@ -{% load i18n %} - -
- -
diff --git a/euth/blueprints/templates/euth_blueprints/includes/infopage.html b/euth/blueprints/templates/euth_blueprints/includes/infopage.html deleted file mode 100644 index de3fb14b7..000000000 --- a/euth/blueprints/templates/euth_blueprints/includes/infopage.html +++ /dev/null @@ -1,91 +0,0 @@ -{% load static i18n infopage wagtailsettings_tags %} -{% get_settings %} - - diff --git a/euth/blueprints/templates/euth_blueprints/includes/rangebuttons.html b/euth/blueprints/templates/euth_blueprints/includes/rangebuttons.html deleted file mode 100644 index 9dee01487..000000000 --- a/euth/blueprints/templates/euth_blueprints/includes/rangebuttons.html +++ /dev/null @@ -1,13 +0,0 @@ -

{{ field.label }}

-{{ field.errors }} - diff --git a/euth/blueprints/templates/euth_blueprints/includes/step-aim.html b/euth/blueprints/templates/euth_blueprints/includes/step-aim.html deleted file mode 100644 index ce5d0b1d3..000000000 --- a/euth/blueprints/templates/euth_blueprints/includes/step-aim.html +++ /dev/null @@ -1,46 +0,0 @@ -{% load i18n static %} - -
-
- - {% trans 'Cancel' %} - -
{% trans 'Decision support tool' %}
-
-

{% trans 'What do you want to do in your participation project?' %}

-
-
{% trans 'My Aim' %}
-
{% trans 'Use Cases' %}
-
- {% for radio in form.aim_choices %} -
-
-
-
- -
-
-
-
-
    - {% for example in radio.example %} -
  • {{ example }}
  • - {% endfor %} -
-
-
- {% endfor %} -
- {% include 'euth_blueprints/includes/breadcrumbs.html' with aim=True %} - -
- -
-
-
diff --git a/euth/blueprints/templates/euth_blueprints/includes/step-feasibility.html b/euth/blueprints/templates/euth_blueprints/includes/step-feasibility.html deleted file mode 100644 index d6003bda5..000000000 --- a/euth/blueprints/templates/euth_blueprints/includes/step-feasibility.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load i18n static %} - -
-
- {% trans 'Back' %} -
{% trans 'Decision support tool' %}
-
-

{% trans 'Feasibility' %}

-

{% trans 'The questions below help us to estimate the effort you will have to invest to make your project a success!' %}


-
-
- {% include 'euth_blueprints/includes/rangebuttons.html' with field=form.participants %} -
-
- {% include 'euth_blueprints/includes/rangebuttons.html' with field=form.scope %} -
-
- {% include 'euth_blueprints/includes/rangebuttons.html' with field=form.duration %} -
-
- {% include 'euth_blueprints/includes/rangebuttons.html' with field=form.accessibility %} -
-
-
- {% include 'euth_blueprints/includes/breadcrumbs.html' with feasibility=True %} - -
- -
-
-
diff --git a/euth/blueprints/templates/euth_blueprints/includes/step-finetuning.html b/euth/blueprints/templates/euth_blueprints/includes/step-finetuning.html deleted file mode 100644 index e116cda03..000000000 --- a/euth/blueprints/templates/euth_blueprints/includes/step-finetuning.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load i18n static %} - -
-
- {% trans 'Back' %} -
{% trans 'Decision support tool' %}
-
-

{% trans 'Fine tuning' %}

-

{% trans 'With the following questions we want to get more detailed information to see which process on OPIN fits best.'%}


-
-
- {% include 'euth_blueprints/includes/rangebuttons.html' with field=form.result %} -
-
- {% include 'euth_blueprints/includes/rangebuttons.html' with field=form.experience %} -
-
- {% include 'euth_blueprints/includes/rangebuttons.html' with field=form.motivation %} -
-
-
- {% include 'euth_blueprints/includes/breadcrumbs.html' with finetuning=True %} - -
- -
-
-
diff --git a/euth/blueprints/templates/euth_blueprints/result.html b/euth/blueprints/templates/euth_blueprints/result.html deleted file mode 100644 index d0ad2b106..000000000 --- a/euth/blueprints/templates/euth_blueprints/result.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends 'base.html' %} -{% load static i18n %} -{% block content %} -
-
-
- {% trans 'Back' %} -
{% trans 'Decision support tool' %}
-
-

- {% blocktrans count counter=blueprints|length %} - This template matches your criterias! - {% plural %} - These templates match your criterias! - {% endblocktrans %} -

-
- {% for blueprint_slug, blueprint, time in blueprints %} -
- {{ blueprint.title }} -

{{ blueprint.title }}

- -
-
    - {% for phase_content in blueprint.content %} -
  • - Phase {{ forloop.counter }}: {{ phase_content.description }} -
  • - {% endfor %} - -
-
- - - - {% include 'euth_blueprints/includes/infopage.html'%} - -
- {% endfor %} -
- -
- {% include 'euth_blueprints/includes/breadcrumbs.html' with result=True %} -
-
-{% endblock %} diff --git a/euth/blueprints/templatetags/infopage.py b/euth/blueprints/templatetags/infopage.py deleted file mode 100644 index d3e8d0d44..000000000 --- a/euth/blueprints/templatetags/infopage.py +++ /dev/null @@ -1,8 +0,0 @@ -from django import template - -register = template.Library() - - -@register.simple_tag -def get_project(helppages, blueprint): - return getattr(helppages, blueprint.type, None) diff --git a/euth/blueprints/urls.py b/euth/blueprints/urls.py deleted file mode 100644 index ee0beef26..000000000 --- a/euth/blueprints/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import re_path - -from . import views - -urlpatterns = [ - re_path(r'^(?P[-\w_]+)/$', - views.SuggestFormView.as_view(), name='blueprints-form'), -] diff --git a/euth/blueprints/views.py b/euth/blueprints/views.py deleted file mode 100644 index 238587f47..000000000 --- a/euth/blueprints/views.py +++ /dev/null @@ -1,123 +0,0 @@ -import math - -from django.views import generic -from rules.contrib import views as rules_views - -from euth.dashboard.mixins import DashboardBaseMixin - -from . import blueprints -from . import forms - - -def custom_round(x): - if x % 1 < 0.5: - return math.floor(x) - else: - return math.ceil(x) - - -def filter_blueprints(aim, result, experience, motivation, - participants, scope, duration, accessibility, - options=blueprints.blueprints, - fallbacks=blueprints.fallbacks): - candidates = [] - - for name, blueprint in options: - if aim not in blueprint.requirements.aims: - continue - - requirements = blueprint.requirements - - if result and experience and motivation: - - req_results = requirements.results - req_experience = requirements.experience - req_motivation = requirements.motivation - - if req_results and result not in req_results: - continue - if req_experience and req_experience.value > experience.value: - continue - if req_motivation and req_motivation.value > motivation.value: - continue - - timeneeded = compute_time_needed( - blueprint, participants, duration, scope, - motivation, accessibility, experience) - candidates.append((name, blueprint, timeneeded)) - - if not candidates: - name = fallbacks[aim] - blueprint = dict(options)[name] - timeneeded = compute_time_needed( - blueprint, participants, duration, scope, - motivation, accessibility, experience) - candidates.append((name, blueprint, timeneeded)) - - return candidates - - -def compute_complexity(blueprint, participants, duration, scope): - - return custom_round(sum(( - blueprint.complexity.participants[0] + - participants.value * blueprint.complexity.participants[1], - blueprint.complexity.duration[0] + - duration.value * blueprint.complexity.duration[1], - blueprint.complexity.scope[0] + - scope.value * blueprint.complexity.scope[1] - ))) - - -def compute_mobilisation(motivation, accessibility): - # modify to match different coding for motivation - return custom_round((5 - motivation.value + accessibility.value) / 2) - - -def compute_time_needed( - blueprint, participants, duration, scope, - motivation, accessibility, experience -): - complexity = compute_complexity(blueprint, participants, duration, scope) - mobilisation = compute_mobilisation(motivation, accessibility) - # modify to match different coding for experience - value = (complexity + 1) * (mobilisation + 5 - experience.value) - - if value < 13: - return 5 - elif value < 21: - return 10 - elif value < 28: - return 15 - elif value < 35: - return 20 - elif value < 40: - return 25 - elif value < 50: - return 30 - else: - return 35 - - -class SuggestFormView(DashboardBaseMixin, - rules_views.PermissionRequiredMixin, - generic.FormView): - template_name = 'euth_blueprints/form.html' - form_class = forms.GetSuggestionForm - permission_required = 'a4projects.add_project' - - def form_valid(self, form): - context = { - 'blueprints': filter_blueprints(**form.cleaned_data), - 'organisation': self.organisation, - 'request': self.request # FIXME: should be done context processor - } - context.update(self.get_context_data()) - return self.response_class( - request=self.request, - template='euth_blueprints/result.html', - context=context - ) - - def get_permission_object(self): - return self.organisation diff --git a/euth/communitydebate/admin.py b/euth/communitydebate/admin.py deleted file mode 100644 index 78c49052c..000000000 --- a/euth/communitydebate/admin.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib import admin - -from euth.communitydebate import models - - -class TopicAdmin(admin.ModelAdmin): - list_filter = ('module__project', 'module') - - -admin.site.register(models.Topic, TopicAdmin) -admin.site.register(models.TopicFileUpload) diff --git a/euth/communitydebate/forms.py b/euth/communitydebate/forms.py deleted file mode 100644 index 07c6ab536..000000000 --- a/euth/communitydebate/forms.py +++ /dev/null @@ -1,40 +0,0 @@ -from django import forms - -from adhocracy4.categories import forms as category_forms -from euth.contrib import widgets -from euth.contrib.mixins import ImageRightOfUseMixin - -from . import models - - -class TopicForm(category_forms.CategorizableFieldMixin, - ImageRightOfUseMixin, - forms.ModelForm): - - class Meta: - model = models.Topic - fields = ['name', 'description', 'image', 'category'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['category'].empty_label = '---' - - class Media: - js = ('fileupload_formset.js',) - - -class TopicFileUploadForm(forms.ModelForm): - - class Meta: - model = models.TopicFileUpload - fields = ['title', 'document'] - widgets = { - 'document': widgets.FileUploadWidget() - } - - -TopicFileUploadFormset = forms.inlineformset_factory(models.Topic, - models.TopicFileUpload, - TopicFileUploadForm, - extra=1, max_num=3, - can_delete=True) diff --git a/euth/communitydebate/migrations/0005_auto_20240909_1609.py b/euth/communitydebate/migrations/0005_auto_20240909_1609.py new file mode 100644 index 000000000..26e401aac --- /dev/null +++ b/euth/communitydebate/migrations/0005_auto_20240909_1609.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.20 on 2024-09-09 14:09 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_communitydebate', '0004_topicfileupload'), + ] + + operations = [ + migrations.DeleteModel( + name='TopicFileUpload', + ), + migrations.DeleteModel( + name='Topic', + ), + + ] diff --git a/euth/communitydebate/migrations/0006_delete_model.py.bak b/euth/communitydebate/migrations/0006_delete_model.py.bak new file mode 100644 index 000000000..dac74728f --- /dev/null +++ b/euth/communitydebate/migrations/0006_delete_model.py.bak @@ -0,0 +1,20 @@ +# Generated by Django 3.2.20 on 2024-09-09 14:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_communitydebate', '0005_auto_20240909_1609'), + ] + + operations = [ + migrations.RunSQL( + sql=[ + "DROP TABLE IF EXISTS euth_communitydebate_topic", + "DROP TABLE IF EXISTS euth_communitydebate_topicfileupload", + ], + reverse_sql=migrations.RunSQL.noop, + ) + ] diff --git a/euth/communitydebate/models.py b/euth/communitydebate/models.py index b4c6fee9b..e69de29bb 100644 --- a/euth/communitydebate/models.py +++ b/euth/communitydebate/models.py @@ -1,64 +0,0 @@ -from autoslug import AutoSlugField -from ckeditor.fields import RichTextField -from django.contrib.contenttypes.fields import GenericRelation -from django.db import models -from django.urls import reverse - -from adhocracy4 import transforms -from adhocracy4.categories.fields import CategoryField -from adhocracy4.comments import models as comment_models -from adhocracy4.images import fields -from adhocracy4.models import base -from adhocracy4.models import query -from adhocracy4.modules import models as module_models -from adhocracy4.ratings import models as rating_models -from euth.contrib import validators - - -class TopicQuerySet(query.RateableQuerySet, query.CommentableQuerySet): - pass - - -class Topic(module_models.Item): - slug = AutoSlugField(populate_from='name', unique=True) - name = models.CharField(max_length=120) - description = RichTextField() - image = fields.ConfiguredImageField( - 'idea_image', - upload_to='communitydebate/images', - blank=True, - ) - ratings = GenericRelation(rating_models.Rating, - related_query_name='topic', - object_id_field='object_pk') - comments = GenericRelation(comment_models.Comment, - related_query_name='topic', - object_id_field='object_pk') - category = CategoryField() - - objects = TopicQuerySet.as_manager() - - def __str__(self): - return self.name - - def save(self, *args, **kwargs): - self.description = transforms.clean_html_field( - self.description) - super(Topic, self).save(*args, **kwargs) - - def get_absolute_url(self): - return reverse('topic-detail', args=[str(self.slug)]) - - -class TopicFileUpload(base.TimeStampedModel): - title = models.CharField(max_length=256) - document = models.FileField( - upload_to='communitydebate/documents', - validators=[validators.validate_file_type_and_size]) - topic = models.ForeignKey( - Topic, - on_delete=models.CASCADE - ) - - def __str__(self): - return self.title diff --git a/euth/communitydebate/phases.py b/euth/communitydebate/phases.py deleted file mode 100644 index 8b9007d94..000000000 --- a/euth/communitydebate/phases.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4 import phases - -from . import apps -from . import models -from . import views - - -class DebatePhase(phases.PhaseContent): - app = apps.Config.label - phase = 'debate' - view = views.TopicListView - - name = _('Debate phase') - description = _('Add topics and debate them.') - module_name = _('communitydebate') - icon = 'far fa-comments' - - features = { - 'crud': (models.Topic,), - 'comment': (models.Topic,), - 'rate': (models.Topic,), - } - - -phases.content.register(DebatePhase()) diff --git a/euth/communitydebate/rules.py b/euth/communitydebate/rules.py deleted file mode 100644 index 01e074574..000000000 --- a/euth/communitydebate/rules.py +++ /dev/null @@ -1,38 +0,0 @@ -import rules -from rules.predicates import is_superuser - -from adhocracy4.modules.predicates import is_context_initiator -from adhocracy4.modules.predicates import is_context_member -from adhocracy4.modules.predicates import is_context_moderator -from adhocracy4.modules.predicates import is_owner -from adhocracy4.modules.predicates import is_public_context -from adhocracy4.phases.predicates import phase_allows_add -from adhocracy4.phases.predicates import phase_allows_change -from adhocracy4.phases.predicates import phase_allows_comment -from adhocracy4.phases.predicates import phase_allows_rate - -from .models import Topic - -rules.add_perm('euth_communitydebate.comment_topic', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_comment)) - - -rules.add_perm('euth_communitydebate.modify_topic', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & is_owner & phase_allows_change)) - - -rules.add_perm('euth_communitydebate.propose_topic', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_add(Topic))) - - -rules.add_perm('euth_communitydebate.rate_topic', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_rate)) - - -rules.add_perm('euth_communitydebate.view_topic', - is_superuser | is_context_moderator | is_context_initiator | - is_context_member | is_public_context) diff --git a/euth/communitydebate/signals.py b/euth/communitydebate/signals.py index 177db1619..e69de29bb 100644 --- a/euth/communitydebate/signals.py +++ b/euth/communitydebate/signals.py @@ -1,28 +0,0 @@ -import os - -from django.db.models.signals import post_delete -from django.db.models.signals import pre_save -from django.dispatch import receiver - -from .models import TopicFileUpload - - -@receiver(post_delete, sender=TopicFileUpload) -def delete_topic_file_upload(sender, instance, **kwargs): - if instance.document: - instance.document.delete(False) - - -@receiver(pre_save, sender=TopicFileUpload) -def delete_topic_file_upload_on_change(sender, instance, **kwargs): - if not instance.pk: - return False - try: - old_file = sender.objects.get(pk=instance.pk).document - except sender.DoesNotExist: - return False - - new_file = instance.document - if not old_file == new_file: - if os.path.isfile(old_file.path): - os.remove(old_file.path) diff --git a/euth/communitydebate/urls.py b/euth/communitydebate/urls.py deleted file mode 100644 index fc2e3fc65..000000000 --- a/euth/communitydebate/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import re_path - -from . import views - -urlpatterns = [ - re_path(r'create/module/(?P[-\w_]+)/$', - views.TopicCreateView.as_view(), name='topic-create'), - re_path(r'^(?P[-\w_]+)/edit/$', - views.TopicUpdateView.as_view(), name='topic-update'), - re_path(r'^(?P[-\w_]+)/delete/$', - views.TopicDeleteView.as_view(), name='topic-delete'), - re_path(r'^(?P[-\w_]+)/$', - views.TopicDetailView.as_view(), name='topic-detail'), -] diff --git a/euth/communitydebate/views.py b/euth/communitydebate/views.py deleted file mode 100644 index e6b4b3f58..000000000 --- a/euth/communitydebate/views.py +++ /dev/null @@ -1,168 +0,0 @@ -from django.contrib import messages -from django.shortcuts import render -from django.urls import reverse -from django.utils.translation import gettext as _ -from django.views import generic -from rules.contrib.views import PermissionRequiredMixin - -from adhocracy4.filters import views as filter_views -from adhocracy4.modules.models import Module -from euth.projects import mixins as prj_mixins - -from . import forms -from . import models as communitydebate_models -from .filters import TopicFilterSet - - -class TopicListView(prj_mixins.ProjectPhaseMixin, - filter_views.FilteredListView): - model = communitydebate_models.Topic - paginate_by = 15 - filter_set = TopicFilterSet - - def get_queryset(self): - return super().get_queryset().filter(module=self.module) - - -class TopicDetailView(PermissionRequiredMixin, generic.DetailView): - model = communitydebate_models.Topic - queryset = \ - communitydebate_models.Topic.objects\ - .annotate_positive_rating_count() \ - .annotate_negative_rating_count() - permission_required = 'euth_communitydebate.view_topic' - - def get_context_data(self, **kwargs): - context = super().get_context_data() - upload_files = communitydebate_models.TopicFileUpload.objects\ - .filter(topic=self.object) - context['upload_files'] = upload_files - return context - - -class TopicCreateView(PermissionRequiredMixin, generic.CreateView): - model = communitydebate_models.Topic - form_class = forms.TopicForm - permission_required = 'euth_communitydebate.propose_topic' - template_name = 'euth_communitydebate/topic_form.html' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - def dispatch(self, *args, **kwargs): - mod_slug = self.kwargs[self.slug_url_kwarg] - self.module = Module.objects.get(slug=mod_slug) - self.project = self.module.project - return super().dispatch(*args, **kwargs) - - def get_permission_object(self, *args, **kwargs): - return self.module - - def get_context_data(self, upload_forms=None, **kwargs): - context = super().get_context_data(**kwargs) - context['project'] = self.project - context['mode'] = 'create' - if not upload_forms: - upload_forms = forms.TopicFileUploadFormset() - context['upload_forms'] = upload_forms - return context - - def form_valid(self, form): - form.instance.creator = self.request.user - form.instance.module = self.module - return super().form_valid(form) - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['module'] = self.module - return kwargs - - def post(self, request, *args, **kwargs): - self.object = None - form = self.get_form() - if form.is_valid(): - topic = form.save(commit=False) - upload_forms = forms.TopicFileUploadFormset(request.POST, - request.FILES, - instance=topic) - if upload_forms.is_valid(): - response = self.form_valid(form) - upload_forms.save() - messages.add_message(request, - messages.SUCCESS, - _('Topic ' - 'successfully created')) - return response - - else: - upload_forms = forms.TopicFileUploadFormset(request.POST, - request.FILES) - return render(request, self.template_name, - self.get_context_data(upload_forms=upload_forms)) - - -class TopicUpdateView(PermissionRequiredMixin, generic.UpdateView): - model = communitydebate_models.Topic - form_class = forms.TopicForm - permission_required = 'euth_communitydebate.modify_topic' - template_name = 'euth_communitydebate/topic_form.html' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - def dispatch(self, *args, **kwargs): - self.object = self.get_object() - return super().dispatch(*args, **kwargs) - - def get_context_data(self, upload_forms=None, **kwargs): - context = super().get_context_data(**kwargs) - context['project'] = self.object.project - context['mode'] = 'update' - if not upload_forms: - upload_forms = forms.TopicFileUploadFormset( - instance=self.get_object()) - context['upload_forms'] = upload_forms - return context - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['module'] = kwargs.get('instance').module - return kwargs - - def post(self, request, *args, **kwargs): - form = self.get_form() - upload_forms = forms.TopicFileUploadFormset(request.POST, - request.FILES, - instance=self.object) - if upload_forms.is_valid() and form.is_valid(): - response = self.form_valid(form) - upload_forms.save() - messages.add_message(request, - messages.SUCCESS, - _('Topic successfully ' - 'updated')) - else: - response = render(request, - self.template_name, - self.get_context_data(upload_forms=upload_forms)) - return response - - -class TopicDeleteView(PermissionRequiredMixin, generic.DeleteView): - model = communitydebate_models.Topic - success_message = _("Your topic has been deleted") - permission_required = 'euth_communitydebate.modify_topic' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - def delete(self, request, *args, **kwargs): - messages.success(self.request, self.success_message) - return super(TopicDeleteView, self).delete(request, *args, **kwargs) - - def get_success_url(self): - return reverse('project-detail', - kwargs={'slug': self.object.project.slug}) diff --git a/euth/dashboard/templates/euth_dashboard/dropdown.html b/euth/dashboard/templates/euth_dashboard/dropdown.html deleted file mode 100644 index 2f2a73502..000000000 --- a/euth/dashboard/templates/euth_dashboard/dropdown.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load thumbnail avatar project_tags i18n rules static %} - - diff --git a/euth/dashboard/templatetags/__init__.py b/euth/dashboard/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/euth/dashboard/templatetags/form_tags.py b/euth/dashboard/templatetags/form_tags.py deleted file mode 100644 index 12f1feebe..000000000 --- a/euth/dashboard/templatetags/form_tags.py +++ /dev/null @@ -1,36 +0,0 @@ -from django import template -from django.conf import settings - -from adhocracy4 import phases - -register = template.Library() - - -@register.simple_tag -def add(number1, number2): - return number1 + number2 - - -@register.simple_tag -def next(some_list, current_index): - try: - return some_list[int(current_index) + 1] - except (IndexError, TypeError, ValueError): - return '' - - -@register.simple_tag -def getPhaseName(type): - name = phases.content.__getitem__(type).name - return name - - -@register.simple_tag -def getAllowedFileTypes(): - fileformats = settings.FILE_ALIASES['*']['fileformats'] - return ', '.join([name for name, mimetype in fileformats]) - - -@register.simple_tag -def get_disabled(project): - return 'disabled' if project and project.is_archived else '' diff --git a/euth/dashboard/templatetags/react_user_list.py b/euth/dashboard/templatetags/react_user_list.py deleted file mode 100644 index 435f7434a..000000000 --- a/euth/dashboard/templatetags/react_user_list.py +++ /dev/null @@ -1,32 +0,0 @@ -import json - -from django import template -from django.db.models.functions import Lower -from django.utils.html import format_html -from rest_framework.serializers import ListSerializer - -from euth.users.serializers import UserWithMailSerializer - -register = template.Library() - - -@register.simple_tag() -def react_user_list(users, project, identifier): - users = users.order_by(Lower('username')) - user_list = ListSerializer(users, child=UserWithMailSerializer()).data - - format_strings = { - 'users': json.dumps(user_list), - 'project': project.pk, - 'listen_to': identifier, - } - - return format_html( - ( - '' - ), - **format_strings - ) diff --git a/euth/documents/admin.py b/euth/documents/admin.py deleted file mode 100644 index e643b2075..000000000 --- a/euth/documents/admin.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib import admin - -from . import models - - -class ParagraphAdmin(admin.ModelAdmin): - list_filter = ('document',) - - -admin.site.register(models.Document) -admin.site.register(models.Paragraph, ParagraphAdmin) diff --git a/euth/documents/api.py b/euth/documents/api.py deleted file mode 100644 index 4b8211047..000000000 --- a/euth/documents/api.py +++ /dev/null @@ -1,27 +0,0 @@ -from rest_framework import mixins -from rest_framework import viewsets - -from adhocracy4.api.mixins import ModuleMixin -from adhocracy4.api.permissions import ViewSetRulesPermission - -from .models import Document -from .serializers import DocumentSerializer - - -class DocumentViewSet(mixins.CreateModelMixin, - mixins.UpdateModelMixin, - ModuleMixin, - viewsets.GenericViewSet): - queryset = Document.objects.all() - serializer_class = DocumentSerializer - permission_classes = (ViewSetRulesPermission,) - - def get_permission_object(self): - return self.module - - def get_serializer_context(self): - context = super().get_serializer_context() - context.update({ - 'module_pk': self.module_pk, - }) - return context diff --git a/euth/documents/exports.py b/euth/documents/exports.py deleted file mode 100644 index 1e80bbc84..000000000 --- a/euth/documents/exports.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.comments.models import Comment -from adhocracy4.exports import mixins as export_mixins -from adhocracy4.exports import views as export_views -from euth.exports import mixins as euth_export_mixins -from euth.exports import register_export - - -@register_export(_('Documents with comments')) -class DocumentExportView( - export_mixins.ExportModelFieldsMixin, - euth_export_mixins.UserGeneratedContentExportMixin, - export_mixins.ItemExportWithLinkMixin, - export_mixins.ItemExportWithRatesMixin, - euth_export_mixins.ItemExportWithRepliesToMixin, - export_views.BaseItemExportView -): - - model = Comment - - fields = ['id', 'comment', 'created'] - - def get_queryset(self): - try: - return self.module.item_set.first().document.clustered_comments - except AttributeError: - return Comment.objects.none() - - def get_base_filename(self): - return '%s_%s' % (self.project.slug, - timezone.now().strftime('%Y%m%dT%H%M%S')) diff --git a/euth/documents/migrations/0003_auto_20240910_1120.py b/euth/documents/migrations/0003_auto_20240910_1120.py new file mode 100644 index 000000000..8f42e2142 --- /dev/null +++ b/euth/documents/migrations/0003_auto_20240910_1120.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.20 on 2024-09-10 09:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_documents', '0002_add_paragraph_ordering_by_weight'), + ] + + operations = [ + migrations.RemoveField( + model_name='paragraph', + name='document', + ), + migrations.DeleteModel( + name='Document', + ), + migrations.DeleteModel( + name='Paragraph', + ), + ] diff --git a/euth/documents/models.py b/euth/documents/models.py index 9dc681097..e69de29bb 100644 --- a/euth/documents/models.py +++ b/euth/documents/models.py @@ -1,82 +0,0 @@ -from ckeditor.fields import RichTextField -from django.contrib.contenttypes.fields import GenericRelation -from django.db import models -from django.utils.functional import cached_property - -from adhocracy4 import transforms -from adhocracy4.comments import models as comment_models -from adhocracy4.models import base -from adhocracy4.modules import models as module_models - -from . import validators - - -class Document(module_models.Item): - name = models.CharField(max_length=120) - comments = GenericRelation(comment_models.Comment, - related_query_name='document', - object_id_field='object_pk') - - def __str__(self): - return "{}_document_{}".format(str(self.module), self.pk) - - def clean(self, *args, **kwargs): - validators.single_document_per_module(self.module, self.pk) - super().clean(*args, **kwargs) - - def get_absolute_url(self): - from django.urls import reverse - return reverse('project-detail', args=[str(self.project.slug)]) - - @property - def clustered_comments(self): - comments = ( - comment_models.Comment.objects.filter( - document__module=self.module) | - comment_models.Comment.objects.filter( - paragraph__document__module=self.module) | - comment_models.Comment.objects.filter( - parent_comment__paragraph__document__module=self.module) | - comment_models.Comment.objects.filter( - parent_comment__document__module=self.module) - ) - return comments - - -class Paragraph(base.TimeStampedModel): - name = models.CharField(max_length=120, blank=True) - text = RichTextField() - weight = models.PositiveIntegerField() - document = models.ForeignKey(Document, - on_delete=models.CASCADE, - related_name='paragraphs') - comments = GenericRelation(comment_models.Comment, - related_query_name='paragraph', - object_id_field='object_pk') - - class Meta: - ordering = ('weight',) - - def __str__(self): - return "{}_paragraph_{}".format(str(self.document), self.weight) - - def save(self, *args, **kwargs): - self.text = transforms.clean_html_field( - self.text, 'image-editor') - super().save(*args, **kwargs) - - def get_absolute_url(self): - from django.urls import reverse - return reverse('paragraph-detail', args=[str(self.pk)]) - - @cached_property - def creator(self): - return self.document.creator - - @cached_property - def module(self): - return self.document.module - - @cached_property - def project(self): - return self.document.project diff --git a/euth/documents/phases.py b/euth/documents/phases.py deleted file mode 100644 index 01fac3f97..000000000 --- a/euth/documents/phases.py +++ /dev/null @@ -1,48 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4 import phases - -from . import apps -from . import models -from . import views - - -class CreateDocumentPhase(phases.PhaseContent): - """ - Allows no interaction for participants, only - creation for moderators. - """ - app = apps.Config.label - phase = 'create_document' - view = views.DocumentCreateView - - name = _('Create document phase') - module_name = _('commenting text') - description = _('Create text for the project.') - icon = 'far fa-file-alt' - - features = {} - - -phases.content.register(CreateDocumentPhase()) - - -class CommentPhase(phases.PhaseContent): - """ - Allows only commenting of paragraphs. - """ - app = apps.Config.label - phase = 'comment' - view = views.DocumentDetailView - - name = _('Comment phase') - module_name = _('commenting text') - description = _('Collect comments for the text.') - icon = 'far fa-comment' - - features = { - 'comment': (models.Paragraph, models.Document), - } - - -phases.content.register(CommentPhase()) diff --git a/euth/documents/rules.py b/euth/documents/rules.py deleted file mode 100644 index 6f4814e2d..000000000 --- a/euth/documents/rules.py +++ /dev/null @@ -1,29 +0,0 @@ -import rules -from rules.predicates import is_superuser - -from adhocracy4.modules.predicates import is_context_initiator -from adhocracy4.modules.predicates import is_context_member -from adhocracy4.modules.predicates import is_context_moderator -from adhocracy4.modules.predicates import is_public_context -from adhocracy4.phases.predicates import phase_allows_comment - -rules.add_perm('euth_documents.comment_paragraph', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_comment)) - -rules.add_perm('euth_documents.comment_document', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_comment)) - -rules.add_perm('euth_documents.add_document', - is_superuser | is_context_moderator | is_context_initiator) - -rules.add_perm('euth_documents.change_document', - is_superuser | is_context_moderator | is_context_initiator) - -rules.add_perm('euth_documents.create_document', - is_superuser | is_context_moderator | is_context_initiator) - -rules.add_perm('euth_documents.view_paragraph', - is_superuser | is_context_moderator | is_context_initiator | - is_context_member | is_public_context) diff --git a/euth/documents/serializers.py b/euth/documents/serializers.py deleted file mode 100644 index e0c9c82f4..000000000 --- a/euth/documents/serializers.py +++ /dev/null @@ -1,65 +0,0 @@ -from rest_framework import serializers - -from adhocracy4.modules.models import Module - -from . import validators -from .models import Document -from .models import Paragraph - - -class ParagraphSerializer(serializers.Serializer): - id = serializers.IntegerField(required=False) - name = serializers.CharField( - required=False, - allow_blank=True, - max_length=Paragraph._meta.get_field('name').max_length - ) - weight = serializers.IntegerField() - text = serializers.CharField() - - -class DocumentSerializer(serializers.ModelSerializer): - paragraphs = ParagraphSerializer(many=True, partial=True) - - class Meta: - model = Document - exclude = ('creator', 'module') - - def validate(self, data): - if self.instance: - document_pk = self.instance.pk - else: - document_pk = None - module_pk = self.context['module_pk'] - module = Module.objects.get(pk=module_pk) - validators.single_document_per_module(module, document_pk) - data['module'] = module - return data - - def create(self, validated_data): - paragraphs = validated_data.pop('paragraphs') - user = self.context['request'].user - document = Document.objects.create(creator=user, **validated_data) - - for paragraph in paragraphs: - Paragraph.objects.create(document=document, **paragraph) - - return document - - def update(self, instance, validated_data): - instance.name = validated_data['name'] - instance.save() - paragraphs = validated_data.pop('paragraphs') - - paragraph_ids = [item['id'] for item in paragraphs if 'id' in item] - instance.paragraphs.exclude(id__in=paragraph_ids).delete() - - for paragraph in paragraphs: - paragraph['document'] = instance - if 'id' in paragraph: - instance.paragraphs.filter(id=paragraph['id'])\ - .update(**paragraph) - else: - instance.paragraphs.create(**paragraph) - - return instance diff --git a/euth/documents/templates/euth_documents/document_detail.html b/euth/documents/templates/euth_documents/document_detail.html deleted file mode 100644 index 38ab23096..000000000 --- a/euth/documents/templates/euth_documents/document_detail.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends 'euth_projects/project_detail.html' %} -{% load i18n react_comments_async %} - -{% block phase_content %} -
- - - {% if object %} -
-
-
- {% trans 'Comments'%} -
-
- {% react_comments_async object %} -
-
-
- - {% endif %} - -
-{% endblock %} diff --git a/euth/documents/templates/euth_documents/document_form.html b/euth/documents/templates/euth_documents/document_form.html deleted file mode 100644 index 1d8783290..000000000 --- a/euth/documents/templates/euth_documents/document_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'euth_projects/project_detail.html' %} -{% load react_paragraphs i18n rules %} - -{% block phase_content %} -
- {% has_perm 'euth_documents.create_document' request.user view.module as show_create_form %} - {% if show_create_form %} - {% react_paragraphs view.document view.module %} - {% else %} - {% trans 'Moderators are currently creating a document.' %} - {% endif %} -
-{% endblock %} diff --git a/euth/documents/templates/euth_documents/paragraph_detail.html b/euth/documents/templates/euth_documents/paragraph_detail.html deleted file mode 100644 index 929e799cd..000000000 --- a/euth/documents/templates/euth_documents/paragraph_detail.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "euth_projects/project_detail.html" %} -{% load i18n react_comments_async react_ratings thumbnail%} - -{% block title %} -{% trans 'Add a comment' %} -{% endblock %} - -{% block content %} -
-
- -

{{ paragraph.name }}

-

{{ paragraph.text | safe }}

-
- {% trans 'Comments'%} -
-
- {% react_comments_async paragraph %} -
-
-
-{% endblock %} diff --git a/euth/documents/templates/euth_documents/react_paragraphs.html b/euth/documents/templates/euth_documents/react_paragraphs.html deleted file mode 100644 index 8a1d915ce..000000000 --- a/euth/documents/templates/euth_documents/react_paragraphs.html +++ /dev/null @@ -1,6 +0,0 @@ -{% load static i18n %} - -
-
-
-
diff --git a/euth/documents/templatetags/__init__.py b/euth/documents/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/euth/documents/templatetags/react_paragraphs.py b/euth/documents/templatetags/react_paragraphs.py deleted file mode 100644 index d7a37be88..000000000 --- a/euth/documents/templatetags/react_paragraphs.py +++ /dev/null @@ -1,27 +0,0 @@ -import json - -from ckeditor_uploader.widgets import CKEditorUploadingWidget -from django import template - -from euth.documents.serializers import DocumentSerializer - -register = template.Library() - - -@register.inclusion_tag('euth_documents/react_paragraphs.html', - takes_context=True) -def react_paragraphs(context, doc, module): - - serializer = DocumentSerializer(doc) - document = serializer.data - widget = CKEditorUploadingWidget(config_name='image-editor') - widget._set_config() - config = widget.config - - context = { - 'document': json.dumps(document), - 'module': module.pk, - 'config': json.dumps(config), - } - - return context diff --git a/euth/documents/urls.py b/euth/documents/urls.py deleted file mode 100644 index 4c076e115..000000000 --- a/euth/documents/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path - -from . import views - -urlpatterns = [ - path('/', views.ParagraphDetailView.as_view(), - name='paragraph-detail'), -] diff --git a/euth/documents/validators.py b/euth/documents/validators.py deleted file mode 100644 index d485d0756..000000000 --- a/euth/documents/validators.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.core.exceptions import ValidationError -from django.utils.translation import gettext as _ - - -def single_document_per_module(module, pk=None): - from .models import Document - siblings = Document.objects.filter(module=module) - - if pk: - siblings = siblings.exclude(pk=pk) - - if len(siblings) != 0: - raise ValidationError( - _('Document for that module already exists') - ) diff --git a/euth/documents/views.py b/euth/documents/views.py deleted file mode 100644 index 3a234db0c..000000000 --- a/euth/documents/views.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.views import generic -from rules.contrib.views import PermissionRequiredMixin - -from euth.projects import mixins as prj_mixins - -from . import models - - -class DocumentCreateView( - prj_mixins.ProjectPhaseMixin, - generic.TemplateView -): - template_name = 'euth_documents/document_form.html' - - @property - def document(self): - return models.Document.objects.filter(module=self.module).first() - - -class DocumentDetailView(generic.DetailView, prj_mixins.ProjectPhaseMixin): - model = models.Document - - def get_object(self): - return models.Document.objects.filter(module=self.module).first() - - -class ParagraphDetailView(PermissionRequiredMixin, generic.DetailView): - permission_required = 'euth_documents.view_paragraph' - model = models.Paragraph - - @property - def raise_exception(self): - return self.request.user.is_authenticated diff --git a/euth/exports/__init__.py b/euth/exports/__init__.py deleted file mode 100644 index eb43aef2c..000000000 --- a/euth/exports/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.apps import apps as django_apps - -default_app_config = 'euth.exports.apps.Config' - - -def register_export(description): - def export_view_decorator(view_cls): - exports.register(description, view_cls) - return lambda cls: cls - return export_view_decorator - - -class ExportsRegistry: - _registry = {} - - def register(self, description, cls): - app_config = django_apps.get_containing_app_config(cls.__module__) - app_label = app_config.label - app_exports = self._registry.get(app_label, []) - app_exports.append((description, cls)) - self._registry[app_label] = sorted(app_exports, key=lambda e: e[1]) - - def __getitem__(self, module): - return self._registry[module.phases.first().content().app] - - def __contains__(self, module): - return module.phases.first().content().app in self._registry - - -exports = ExportsRegistry() diff --git a/euth/exports/apps.py b/euth/exports/apps.py deleted file mode 100644 index e29da0227..000000000 --- a/euth/exports/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.apps import AppConfig - - -class Config(AppConfig): - name = 'euth.exports' - label = 'euth_exports' - - def ready(self): - from django.utils.module_loading import autodiscover_modules - autodiscover_modules('exports', register_to=self.module.exports) diff --git a/euth/exports/dashboard.py b/euth/exports/dashboard.py deleted file mode 100644 index f54ea1d15..000000000 --- a/euth/exports/dashboard.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.dashboard import DashboardComponent -from adhocracy4.dashboard import components - -from . import exports -from . import views - - -class ExportModuleComponent(DashboardComponent): - identifier = 'module_export' - weight = 50 - label = _('Export Excel') - - def is_effective(self, module): - return not module.project.is_draft and module in exports - - def get_progress(self, module): - return 0, 0 - - def get_base_url(self, module): - return reverse('a4dashboard:export-module', kwargs={ - 'module_slug': module.slug, - 'export_id': 0, - }) - - def get_urls(self): - return [ - (r'^modules/(?P[-\w_]+)/export/(?P\d+)/$', - views.ExportModuleDispatcher.as_view(), - 'export-module'), - ] - - -components.register_module(ExportModuleComponent()) diff --git a/euth/exports/mixins.py b/euth/exports/mixins.py deleted file mode 100644 index f6861aa44..000000000 --- a/euth/exports/mixins.py +++ /dev/null @@ -1,54 +0,0 @@ -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import gettext as _ - -from adhocracy4.exports.views import VirtualFieldMixin - - -class ItemExportWithLocationMixin(VirtualFieldMixin): - def get_virtual_fields(self, virtual): - if 'location_lon' not in virtual: - virtual['location_lon'] = _('Location (Longitude)') - if 'location_lat' not in virtual: - virtual['location_lat'] = _('Location (Latitude)') - return super().get_virtual_fields(virtual) - - def get_location_lon_data(self, item): - if hasattr(item, 'point'): - point = item.point - if 'geometry' in point: - return point['geometry']['coordinates'][0] - return '' - - def get_location_lat_data(self, item): - if hasattr(item, 'point'): - point = item.point - if 'geometry' in point: - return point['geometry']['coordinates'][1] - return '' - - -class ItemExportWithRepliesToMixin(VirtualFieldMixin): - def get_virtual_fields(self, virtual): - virtual['replies_to'] = _('replies to') - return super().get_virtual_fields(virtual) - - def get_replies_to_data(self, comment): - try: - return comment.parent_comment.get().pk - except ObjectDoesNotExist: - return '' - - -class UserGeneratedContentExportMixin(VirtualFieldMixin): - def get_virtual_fields(self, virtual): - if 'creator' not in virtual: - virtual['creator'] = _('Creator') - if 'created' not in virtual: - virtual['created'] = _('Created') - return super().get_virtual_fields(virtual) - - def get_creator_data(self, item): - return item.creator.username - - def get_created_data(self, item): - return item.created.isoformat() diff --git a/euth/exports/views.py b/euth/exports/views.py deleted file mode 100644 index 6afe42457..000000000 --- a/euth/exports/views.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.views import generic - -from adhocracy4.modules import models as module_models -from adhocracy4.rules import mixins as rules_mixins - -from . import exports - - -class ExportModuleDispatcher(rules_mixins.PermissionRequiredMixin, - generic.View): - permission_required = 'a4projects.change_project' - - def dispatch(self, request, *args, **kwargs): - export_id = int(kwargs.pop('export_id')) - module = module_models.Module.objects.get(slug=kwargs['module_slug']) - project = module.project - - self.project = project - - # Since the PermissionRequiredMixin.dispatch method is never called - # we have to check permissions manually - if not self.has_permission(): - return self.handle_no_permission() - - module_exports = exports[module] - assert len(module_exports) > export_id - - # Dispatch the request to the export view - view = module_exports[export_id][1].as_view() - return view(request, module=module, *args, **kwargs) - - def get_permission_object(self): - return self.project diff --git a/euth/follows/admin.py b/euth/follows/admin.py deleted file mode 100644 index 341c9d3a6..000000000 --- a/euth/follows/admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from . import models - - -class FollowAdmin(admin.ModelAdmin): - list_display = ('__str__', 'creator', 'project', 'enabled') - - -admin.site.register(models.Follow, FollowAdmin) diff --git a/euth/follows/api.py b/euth/follows/api.py deleted file mode 100644 index 245adc832..000000000 --- a/euth/follows/api.py +++ /dev/null @@ -1,26 +0,0 @@ -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import mixins -from rest_framework import permissions -from rest_framework import viewsets - -from euth.contrib.api.mixins import AllowPUTAsCreateMixin - -from . import models -from .serializers import FollowSerializer - - -class FollowViewSet(AllowPUTAsCreateMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet): - - lookup_field = 'project__slug' - queryset = models.Follow.objects - serializer_class = FollowSerializer - permission_classes = (permissions.IsAuthenticated,) - filter_backends = (DjangoFilterBackend,) - filterset_fields = ('enabled', ) - - def get_queryset(self): - return self.queryset.filter(creator=self.request.user) diff --git a/euth/follows/migrations/0002_delete_follow.py b/euth/follows/migrations/0002_delete_follow.py new file mode 100644 index 000000000..3d6d16a96 --- /dev/null +++ b/euth/follows/migrations/0002_delete_follow.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.20 on 2024-09-10 09:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_follows', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='Follow', + ), + ] diff --git a/euth/follows/models.py b/euth/follows/models.py index 410b2a17a..e69de29bb 100644 --- a/euth/follows/models.py +++ b/euth/follows/models.py @@ -1,18 +0,0 @@ -from django.db import models - -from adhocracy4.models import base -from adhocracy4.projects import models as prj_models - - -class Follow(base.UserGeneratedContentModel): - project = models.ForeignKey( - prj_models.Project, - on_delete=models.CASCADE - ) - enabled = models.BooleanField(default=True) - - class Meta: - unique_together = ('project', 'creator') - - def __str__(self): - return 'Follow({}, enabled={})'.format(self.project, self.enabled) diff --git a/euth/follows/serializers.py b/euth/follows/serializers.py deleted file mode 100644 index 1925494f0..000000000 --- a/euth/follows/serializers.py +++ /dev/null @@ -1,32 +0,0 @@ -from rest_framework import serializers - -from adhocracy4.projects import models as prj_models - -from . import models - - -class FollowSerializer(serializers.ModelSerializer): - - project = serializers.SlugRelatedField(read_only=True, slug_field='slug') - follows = serializers.SerializerMethodField() - creator = serializers.HiddenField( - default=serializers.CurrentUserDefault() - ) - - class Meta: - model = models.Follow - read_only_field = ('project', 'creator', 'created', 'modified') - exclude = ('id',) - - def save(self, *args, **kwargs): - if 'project__slug' in kwargs: - slug = kwargs.pop('project__slug') - project = prj_models.Project.objects.get(slug=slug) - kwargs['project'] = project - return super().save(*args, **kwargs) - - def get_follows(self, obj): - return models.Follow.objects.filter( - project=obj.project, - enabled=True - ).count() diff --git a/euth/follows/signals.py b/euth/follows/signals.py index 2c05bab15..e69de29bb 100644 --- a/euth/follows/signals.py +++ b/euth/follows/signals.py @@ -1,17 +0,0 @@ -from django.db.models.signals import post_save - -from . import models - - -def autofollow_hook(instance, **kwargs): - if hasattr(instance.project, 'id'): - models.Follow.objects.get_or_create( - project=instance.project, - creator=instance.creator, - defaults={ - 'enabled': True, - }) - - -post_save.connect(autofollow_hook, 'a4comments.Comment') -post_save.connect(autofollow_hook, 'euth_ideas.Idea') diff --git a/euth/ideas/admin.py b/euth/ideas/admin.py deleted file mode 100644 index c3d5c668b..000000000 --- a/euth/ideas/admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from . import models - - -class IdeaAdmin(admin.ModelAdmin): - list_filter = ('module__project', 'module') - - -admin.site.register(models.Idea, IdeaAdmin) diff --git a/euth/ideas/exports.py b/euth/ideas/exports.py deleted file mode 100644 index 7c87774dd..000000000 --- a/euth/ideas/exports.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.exports import mixins as a4_export_mixins -from adhocracy4.exports import views as a4_export_views -from euth.exports import register_export - -from . import models - - -@register_export(_('Ideas with comments')) -class IdeaExportView(a4_export_mixins.ItemExportWithLinkMixin, - a4_export_mixins.ExportModelFieldsMixin, - a4_export_mixins.ItemExportWithRatesMixin, - a4_export_mixins.ItemExportWithCategoriesMixin, - a4_export_mixins.ItemExportWithCommentCountMixin, - a4_export_mixins.ItemExportWithCommentsMixin, - a4_export_views.BaseItemExportView): - - model = models.Idea - fields = ['name', 'description'] - html_fields = ['description'] - - def get_queryset(self): - return super().get_queryset() \ - .filter(module=self.module)\ - .annotate_comment_count()\ - .annotate_positive_rating_count()\ - .annotate_negative_rating_count() diff --git a/euth/ideas/filters.py b/euth/ideas/filters.py deleted file mode 100644 index a8ccb216d..000000000 --- a/euth/ideas/filters.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.categories import filters as cat_filters -from adhocracy4.filters import widgets -from adhocracy4.filters.filters import DefaultsFilterSet -from adhocracy4.filters.filters import DynamicChoicesOrderingFilter -from euth.ideas.models import Idea - -ORDERING_CHOICES = [ - ('newest', _('Most Recent')), - ('comments', _('Most Comments')), - ('support', _('Most Support')) -] - - -class OrderingFilterWidget(widgets.DropdownLinkWidget): - label = _('Sort by') - - -class IdeaFilterSet(DefaultsFilterSet): - - defaults = { - 'ordering': 'newest' - } - - category = cat_filters.CategoryFilter() - - ordering = DynamicChoicesOrderingFilter( - fields=( - ('-created', 'newest'), - ('-comment_count', 'comments'), - ('-positive_rating_count', 'support') - ), - choices=ORDERING_CHOICES, - empty_label=None, - widget=OrderingFilterWidget - ) - - class Meta: - model = Idea - fields = ['category', 'ordering'] diff --git a/euth/ideas/forms.py b/euth/ideas/forms.py deleted file mode 100644 index 139ac600d..000000000 --- a/euth/ideas/forms.py +++ /dev/null @@ -1,19 +0,0 @@ -from django import forms - -from adhocracy4.categories import forms as category_forms -from euth.contrib.mixins import ImageRightOfUseMixin - -from . import models - - -class IdeaForm(category_forms.CategorizableFieldMixin, - ImageRightOfUseMixin, - forms.ModelForm): - - class Meta: - model = models.Idea - fields = ['name', 'description', 'image', 'category'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['category'].empty_label = '---' diff --git a/euth/ideas/migrations/0007_delete_idea.py b/euth/ideas/migrations/0007_delete_idea.py new file mode 100644 index 000000000..cd40b7ea3 --- /dev/null +++ b/euth/ideas/migrations/0007_delete_idea.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.20 on 2024-09-10 09:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_maps', '0006_delete_mapidea'), + ('euth_ideas', '0006_alter_idea_image'), + ] + + operations = [ + migrations.DeleteModel( + name='Idea', + ), + ] diff --git a/euth/ideas/models.py b/euth/ideas/models.py index 9bac9bacc..e69de29bb 100644 --- a/euth/ideas/models.py +++ b/euth/ideas/models.py @@ -1,53 +0,0 @@ -from autoslug import AutoSlugField -from ckeditor.fields import RichTextField -from django.contrib.contenttypes.fields import GenericRelation -from django.db import models -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from adhocracy4 import transforms -from adhocracy4.categories.fields import CategoryField -from adhocracy4.comments import models as comment_models -from adhocracy4.images import fields -from adhocracy4.models import query -from adhocracy4.modules import models as module_models -from adhocracy4.ratings import models as rating_models - - -class IdeaQuerySet(query.RateableQuerySet, query.CommentableQuerySet): - pass - - -class Idea(module_models.Item): - slug = AutoSlugField(populate_from='name', unique=True) - name = models.CharField(max_length=120) - description = RichTextField() - image = fields.ConfiguredImageField( - 'idea_image', - upload_to='ideas/images', - blank=True, - help_prefix=_( - 'Please make sure the image is at least 400x200 px in ' - 'dimensions and at most 5 MB in file size.' - ), - ) - ratings = GenericRelation(rating_models.Rating, - related_query_name='idea', - object_id_field='object_pk') - comments = GenericRelation(comment_models.Comment, - related_query_name='idea', - object_id_field='object_pk') - category = CategoryField() - - objects = IdeaQuerySet.as_manager() - - def __str__(self): - return self.name - - def save(self, *args, **kwargs): - self.description = transforms.clean_html_field( - self.description) - super(Idea, self).save(*args, **kwargs) - - def get_absolute_url(self): - return reverse('idea-detail', args=[str(self.slug)]) diff --git a/euth/ideas/phases.py b/euth/ideas/phases.py deleted file mode 100644 index 27325670c..000000000 --- a/euth/ideas/phases.py +++ /dev/null @@ -1,94 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4 import phases - -from . import apps -from . import models -from . import views - - -class IssuePhase(phases.PhaseContent): - app = apps.Config.label - phase = 'issue' - view = views.IdeaListView - - name = _('Issue phase') - description = _('Add new ideas.') - module_name = _('ideas collection') - icon = 'far fa-lightbulb' - - features = { - 'crud': (models.Idea,), - } - - -class CollectPhase(phases.PhaseContent): - app = apps.Config.label - phase = 'collect' - view = views.IdeaListView - - name = _('Collect phase') - description = _('Add new ideas and comment them.') - module_name = _('ideas collection') - icon = 'far fa-lightbulb' - - features = { - 'crud': (models.Idea,), - 'comment': (models.Idea,), - } - - -class RatingPhase(phases.PhaseContent): - app = apps.Config.label - phase = 'rating' - view = views.IdeaListView - - name = _('Rating phase') - module_name = _('ideas collection') - description = _('Get quantative feeback by rating the collected ideas.') - icon = 'fas fa-chevron-up' - - features = { - 'rate': (models.Idea,) - } - - -class FeedbackPhase(phases.PhaseContent): - app = apps.Config.label - phase = 'feedback' - view = views.IdeaListView - - name = _('Feedback phase') - description = _('Get feedback for collected ideas through rates and ' - 'comments.') - module_name = _('ideas collection') - icon = 'far fa-comment' - - features = { - 'rate': (models.Idea,), - 'comment': (models.Idea,) - } - - -class UniversalPhase(phases.PhaseContent): - app = apps.Config.label - phase = 'universal' - view = views.IdeaListView - - name = _('Universal phase') - module_name = _('ideas collection') - description = _('Use all features of the idea collection.') - icon = 'far fa-lightbulb' - - features = { - 'crud': (models.Idea,), - 'comment': (models.Idea,), - 'rate': (models.Idea,), - } - - -phases.content.register(IssuePhase()) -phases.content.register(CollectPhase()) -phases.content.register(RatingPhase()) -phases.content.register(FeedbackPhase()) -phases.content.register(UniversalPhase()) diff --git a/euth/ideas/rules.py b/euth/ideas/rules.py deleted file mode 100644 index 53ce71a16..000000000 --- a/euth/ideas/rules.py +++ /dev/null @@ -1,41 +0,0 @@ -import rules -from rules.predicates import is_superuser - -from adhocracy4.modules.predicates import is_context_initiator -from adhocracy4.modules.predicates import is_context_member -from adhocracy4.modules.predicates import is_context_moderator -from adhocracy4.modules.predicates import is_owner -from adhocracy4.modules.predicates import is_public_context -from adhocracy4.phases.predicates import phase_allows_add -from adhocracy4.phases.predicates import phase_allows_change -from adhocracy4.phases.predicates import phase_allows_comment -from adhocracy4.phases.predicates import phase_allows_rate - -from .models import Idea - -rules.add_perm('euth_ideas.rate_idea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_rate)) - - -rules.add_perm('euth_ideas.comment_idea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_comment)) - - -rules.add_perm('euth_ideas.modify_idea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & is_owner & phase_allows_change)) - - -rules.add_perm('euth_ideas.propose_idea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_add(Idea))) - - -rules.add_perm('euth_ideas.view_idea', - is_superuser | is_context_moderator | is_context_initiator | - is_context_member | is_public_context) - -rules.add_perm('euth_ideas.export_ideas', - is_superuser | is_context_moderator | is_context_initiator) diff --git a/euth/ideas/signals.py b/euth/ideas/signals.py index 61d23c86a..e69de29bb 100644 --- a/euth/ideas/signals.py +++ b/euth/ideas/signals.py @@ -1,25 +0,0 @@ -from django.db.models.signals import post_delete -from django.db.models.signals import post_init -from django.db.models.signals import post_save -from django.dispatch import receiver - -from adhocracy4.images import services as image_services - -from .models import Idea - - -@receiver(post_init, sender=Idea) -def backup_image_path(sender, instance, **kwargs): - instance._current_image_file = instance.image - - -@receiver(post_save, sender=Idea) -def delete_old_image(sender, instance, **kwargs): - if hasattr(instance, '_current_image_file'): - if instance._current_image_file != instance.image: - image_services.delete_images([instance._current_image_file]) - - -@receiver(post_delete, sender=Idea) -def delete_images_for_Idea(sender, instance, **kwargs): - image_services.delete_images([instance.image]) diff --git a/euth/ideas/templatetags/__init__.py b/euth/ideas/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/euth/ideas/templatetags/idea_tags.py b/euth/ideas/templatetags/idea_tags.py deleted file mode 100644 index 3897f44f2..000000000 --- a/euth/ideas/templatetags/idea_tags.py +++ /dev/null @@ -1,29 +0,0 @@ -from django import template - -from euth.ideas.models import Idea - -register = template.Library() - - -@register.simple_tag -def get_range(number, listcount): - if number < 3: - return range(1, 6) - elif number > listcount - 2: - return range(listcount - 4, listcount + 1) - else: - return range(number - 2, number + 3) - - -@register.simple_tag -def is_idea_list(module): - return Idea.objects.filter(module=module).count() > 0 - - -@register.simple_tag -def combined_url_parameter(request_query_dict, **kwargs): - combined_query_dict = request_query_dict.copy() - for key in kwargs: - combined_query_dict.setlist(key, [kwargs[key]]) - encoded_parameter = '?' + combined_query_dict.urlencode() - return encoded_parameter diff --git a/euth/ideas/urls.py b/euth/ideas/urls.py deleted file mode 100644 index 99255cac4..000000000 --- a/euth/ideas/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import re_path - -from . import views - -urlpatterns = [ - re_path(r'create/module/(?P[-\w_]+)/$', - views.IdeaCreateView.as_view(), name='idea-create'), - re_path(r'^(?P[-\w_]+)/edit/$', - views.IdeaUpdateView.as_view(), name='idea-update'), - re_path(r'^(?P[-\w_]+)/delete/$', - views.IdeaDeleteView.as_view(), name='idea-delete'), - re_path(r'^(?P[-\w_]+)/$', - views.IdeaDetailView.as_view(), name='idea-detail'), -] diff --git a/euth/ideas/views.py b/euth/ideas/views.py deleted file mode 100644 index bcd0ecebb..000000000 --- a/euth/ideas/views.py +++ /dev/null @@ -1,118 +0,0 @@ -from django.contrib import messages -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.translation import gettext as _ -from django.views import generic -from rules.contrib.views import PermissionRequiredMixin - -from adhocracy4.filters import views as filter_views -from adhocracy4.modules.models import Module -from euth.projects import mixins as prj_mixins - -from . import forms -from . import models as idea_models -from .filters import IdeaFilterSet - - -class IdeaListView( - prj_mixins.ProjectPhaseMixin, - filter_views.FilteredListView -): - model = idea_models.Idea - paginate_by = 15 - filter_set = IdeaFilterSet - - def get_queryset(self): - return super().get_queryset().filter(module=self.module) - - -class IdeaDetailView(PermissionRequiredMixin, generic.DetailView): - model = idea_models.Idea - queryset = idea_models.Idea.objects.annotate_positive_rating_count() \ - .annotate_negative_rating_count() - permission_required = 'euth_ideas.view_idea' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['author_is_moderator'] = self.object.creator in self.object. \ - project.moderators.all() - return context - - -class IdeaUpdateView(PermissionRequiredMixin, generic.UpdateView): - model = idea_models.Idea - form_class = forms.IdeaForm - permission_required = 'euth_ideas.modify_idea' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['project'] = self.object.project - context['mode'] = 'update' - return context - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['module'] = kwargs.get('instance').module - return kwargs - - -class IdeaCreateView(PermissionRequiredMixin, generic.CreateView): - model = idea_models.Idea - form_class = forms.IdeaForm - permission_required = 'euth_ideas.propose_idea' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - def dispatch(self, *args, **kwargs): - mod_slug = self.kwargs[self.slug_url_kwarg] - self.module = get_object_or_404(Module, slug=mod_slug) - self.project = self.module.project - return super().dispatch(*args, **kwargs) - - def get_permission_object(self, *args, **kwargs): - return self.module - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['slug'] = self.module.slug - context['project'] = self.project - context['mode'] = 'create' - return context - - def form_valid(self, form): - form.instance.creator = self.request.user - form.instance.module = self.module - return super().form_valid(form) - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['module'] = self.module - return kwargs - - -class IdeaDeleteView(PermissionRequiredMixin, generic.DeleteView): - model = idea_models.Idea - success_message = _("Your Idea has been deleted") - permission_required = 'euth_ideas.modify_idea' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - def delete(self, request, *args, **kwargs): - messages.success(self.request, self.success_message) - return super(IdeaDeleteView, self).delete(request, *args, **kwargs) - - def get_success_url(self): - return reverse('project-detail', - kwargs={'slug': self.object.project.slug}) diff --git a/euth/maps/admin.py b/euth/maps/admin.py deleted file mode 100644 index d4b0ce711..000000000 --- a/euth/maps/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin - -from . import models - -admin.site.register(models.MapIdea) diff --git a/euth/maps/exports.py b/euth/maps/exports.py deleted file mode 100644 index b6fd5c8b8..000000000 --- a/euth/maps/exports.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.exports import mixins as a4_export_mixins -from adhocracy4.exports import views as a4_export_views -from euth.exports import register_export -from euth.exports.mixins import ItemExportWithLocationMixin - -from . import models - - -@register_export(_('MapIdeas with comments')) -class MapIdeaExportView(a4_export_mixins.ItemExportWithLinkMixin, - a4_export_mixins.ExportModelFieldsMixin, - ItemExportWithLocationMixin, - a4_export_mixins.ItemExportWithRatesMixin, - a4_export_mixins.ItemExportWithCategoriesMixin, - a4_export_mixins.ItemExportWithCommentCountMixin, - a4_export_mixins.ItemExportWithCommentsMixin, - a4_export_views.BaseItemExportView): - - model = models.MapIdea - fields = ['name', 'description'] - html_fields = ['description'] - - def get_queryset(self): - return super().get_queryset() \ - .filter(module=self.module)\ - .annotate_comment_count()\ - .annotate_positive_rating_count()\ - .annotate_negative_rating_count() diff --git a/euth/maps/forms.py b/euth/maps/forms.py deleted file mode 100644 index 704994184..000000000 --- a/euth/maps/forms.py +++ /dev/null @@ -1,25 +0,0 @@ -from django import forms -from django.utils.translation import gettext as _ - -from adhocracy4.categories import forms as category_forms -from adhocracy4.maps import widgets -from euth.contrib.mixins import ImageRightOfUseMixin - -from . import models - - -class MapIdeaForm(category_forms.CategorizableFieldMixin, - ImageRightOfUseMixin, - forms.ModelForm): - - class Meta: - model = models.MapIdea - fields = ['name', 'description', 'image', 'point', 'category'] - - def __init__(self, *args, **kwargs): - self.settings = kwargs.pop('settings_instance') - super().__init__(*args, **kwargs) - self.fields['point'].widget = widgets.MapChoosePointWidget( - polygon=self.settings.polygon) - self.fields['point'].error_messages['required'] = _( - 'Please locate your proposal on the map.') diff --git a/euth/maps/migrations/0006_delete_mapidea.py b/euth/maps/migrations/0006_delete_mapidea.py new file mode 100644 index 000000000..02858b78e --- /dev/null +++ b/euth/maps/migrations/0006_delete_mapidea.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.20 on 2024-09-10 09:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_maps', '0005_move_model_change_point_delete_areasettings'), + ] + + operations = [ + migrations.DeleteModel( + name='MapIdea', + ), + ] diff --git a/euth/maps/models.py b/euth/maps/models.py index d5dce00b9..e69de29bb 100644 --- a/euth/maps/models.py +++ b/euth/maps/models.py @@ -1,17 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.maps import fields as map_fields -from euth.ideas import models as idea_models - - -class MapIdea(idea_models.Idea): - point = map_fields.PointField( - verbose_name=_('Where can your idea be located on a map?'), - help_text=_('Click inside marked area to set a marker. ' - 'Drag and drop marker to change place.')) - - objects = idea_models.IdeaQuerySet.as_manager() - - def get_absolute_url(self): - from django.urls import reverse - return reverse('map-idea-detail', args=[str(self.slug)]) diff --git a/euth/maps/phases.py b/euth/maps/phases.py deleted file mode 100644 index d85995ee5..000000000 --- a/euth/maps/phases.py +++ /dev/null @@ -1,61 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from adhocracy4 import phases - -from . import apps -from . import models -from . import views - - -class IssuePhase(phases.PhaseContent): - app = apps.Config.label - phase = 'issue' - view = views.MapIdeaListView - - name = _('Issue Phase') - description = _('Add, comment and rate new ideas on a map.') - module_name = _('ideas collection') - icon = 'far fa-map' - - features = { - 'crud': (models.MapIdea,), - 'comment': (models.MapIdea,), - 'rate': (models.MapIdea,), - } - - -class CollectPhase(phases.PhaseContent): - app = apps.Config.label - phase = 'collect' - view = views.MapIdeaListView - - name = _('Collect Phase') - description = _('Add and comment new ideas on a map.') - module_name = _('ideas collection') - icon = 'far fa-map' - - features = { - 'crud': (models.MapIdea,), - 'comment': (models.MapIdea,), - } - - -class RatingPhase(phases.PhaseContent): - app = apps.Config.label - phase = 'rating' - view = views.MapIdeaListView - - name = _('Rating Phase') - description = _('Get quantative feeback by rating the collected ' - 'ideas on a map.') - module_name = _('ideas collection') - icon = 'fas fa-chevron-up' - - features = { - 'rate': (models.MapIdea,), - } - - -phases.content.register(IssuePhase()) -phases.content.register(CollectPhase()) -phases.content.register(RatingPhase()) diff --git a/euth/maps/rules.py b/euth/maps/rules.py deleted file mode 100644 index d9cfed86a..000000000 --- a/euth/maps/rules.py +++ /dev/null @@ -1,38 +0,0 @@ -import rules -from rules.predicates import is_superuser - -from adhocracy4.modules.predicates import is_context_initiator -from adhocracy4.modules.predicates import is_context_member -from adhocracy4.modules.predicates import is_context_moderator -from adhocracy4.modules.predicates import is_owner -from adhocracy4.modules.predicates import is_public_context -from adhocracy4.phases.predicates import phase_allows_add -from adhocracy4.phases.predicates import phase_allows_change -from adhocracy4.phases.predicates import phase_allows_comment -from adhocracy4.phases.predicates import phase_allows_rate - -from .models import MapIdea - -rules.add_perm('euth_maps.rate_mapidea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_rate)) - - -rules.add_perm('euth_maps.comment_mapidea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_comment)) - - -rules.add_perm('euth_maps.modify_mapidea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & is_owner & phase_allows_change)) - - -rules.add_perm('euth_maps.propose_mapidea', - is_superuser | is_context_moderator | is_context_initiator | - (is_context_member & phase_allows_add(MapIdea))) - - -rules.add_perm('euth_maps.view_mapidea', - is_superuser | is_context_moderator | is_context_initiator | - is_context_member | is_public_context) diff --git a/euth/maps/urls.py b/euth/maps/urls.py deleted file mode 100644 index e7d60914a..000000000 --- a/euth/maps/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import re_path - -from . import views - -urlpatterns = [ - re_path(r'create/module/(?P[-\w_]+)/$', - views.MapIdeaCreateView.as_view(), name='map-idea-create'), - re_path(r'^(?P[-\w_]+)/edit/$', - views.MapIdeaUpdateView.as_view(), name='map-idea-update'), - re_path(r'^(?P[-\w_]+)/delete/$', - views.MapIdeaDeleteView.as_view(), name='map-idea-delete'), - re_path(r'^(?P[-\w_]+)/$', - views.MapIdeaDetailView.as_view(), name='map-idea-detail'), -] diff --git a/euth/maps/views.py b/euth/maps/views.py deleted file mode 100644 index 7f1acb842..000000000 --- a/euth/maps/views.py +++ /dev/null @@ -1,43 +0,0 @@ -from euth.ideas import views as idea_views - -from . import forms -from .models import MapIdea - - -class MapIdeaListView(idea_views.IdeaListView): - model = MapIdea - - -class MapIdeaCreateView(idea_views.IdeaCreateView): - model = MapIdea - form_class = forms.MapIdeaForm - permission_required = 'euth_maps.propose_mapidea' - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['settings_instance'] = self.module.settings_instance - return kwargs - - -class MapIdeaUpdateView(idea_views.IdeaUpdateView): - model = MapIdea - permission_required = 'euth_maps.modify_mapidea' - form_class = forms.MapIdeaForm - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['settings_instance'] = self.object.module.settings_instance - return kwargs - - -class MapIdeaDeleteView(idea_views.IdeaDeleteView): - model = MapIdea - permission_required = 'euth_maps.modify_mapidea' - - -class MapIdeaDetailView(idea_views.IdeaDetailView): - model = MapIdea - permission_required = 'euth_maps.view_mapidea' - queryset = MapIdea.objects\ - .annotate_positive_rating_count()\ - .annotate_negative_rating_count() diff --git a/euth/maps/widgets.py b/euth/maps/widgets.py deleted file mode 100644 index e9c4025cd..000000000 --- a/euth/maps/widgets.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.conf import settings -from django.forms.widgets import Widget -from django.template import loader -from django.utils.safestring import mark_safe - - -class MapChoosePolygonWidget(Widget): - - class Media: - js = ( - 'leaflet.js', - 'a4maps_choose_polygon.js', - ) - - css = {'all': [ - 'a4maps_choose_polygon.css', - ]} - - def render(self, name, value, attrs, renderer=None): - - context = { - 'map_url': settings.BASE_MAP, - 'bbox': settings.MAP_BOUNDING_BOX, - 'name': name, - 'polygon': value - } - - return mark_safe( - loader.render_to_string( - 'euth_maps/map_choose_polygon_widget.html', - context - ) - ) - - -class MapChoosePointWidget(Widget): - - def __init__(self, polygon, attrs=None): - self.polygon = polygon - super().__init__(attrs) - - class Media: - js = ( - 'leaflet.js', - 'a4maps_choose_point.js', - ) - css = {'all': [ - 'leaflet.css', - 'a4maps_choose_point.css' - ]} - - def render(self, name, value, attrs, renderer=None): - - context = { - 'map_url': settings.BASE_MAP, - 'bbox': settings.MAP_BOUNDING_BOX, - 'name': name, - 'point': value, - 'polygon': self.polygon - } - - return mark_safe( - loader.render_to_string( - 'euth_maps/map_choose_point_widget.html', - context - ) - ) diff --git a/euth/memberships/admin.py b/euth/memberships/admin.py deleted file mode 100644 index 6aac8efae..000000000 --- a/euth/memberships/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin - -from . import models - -admin.site.register(models.Request) -admin.site.register(models.Invite) diff --git a/euth/memberships/dashboard.py b/euth/memberships/dashboard.py deleted file mode 100644 index d14b094fb..000000000 --- a/euth/memberships/dashboard.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.dashboard import DashboardComponent -from adhocracy4.dashboard import components - -from . import views - - -class MembershipRequestComponent(DashboardComponent): - identifier = 'members' - weight = 10 - label = _('Members') - - def is_effective(self, project_or_module): - return project_or_module.is_private or project_or_module.is_semipublic - - def get_base_url(self, project): - return reverse('a4dashboard:members', kwargs={ - 'project_slug': project.slug - }) - - def get_urls(self): - return [ - (r'^members/project/(?P[-\w_]+)/$', - views.MembershipsDashboardView.as_view(component=self), - 'members'), - ] - - -class MembershipInvitesComponent(DashboardComponent): - identifier = 'invites' - weight = 10 - label = _('Invited Members') - - def is_effective(self, project_or_module): - return project_or_module.is_private or project_or_module.is_semipublic - - def get_base_url(self, project): - return reverse('a4dashboard:invites', kwargs={ - 'project_slug': project.slug - }) - - def get_urls(self): - return [ - (r'^invites/project/(?P[-\w_]+)/$', - views.InviteDashboardView.as_view(component=self), - 'invites'), - ] - - -components.register_project(MembershipRequestComponent()) -components.register_project(MembershipInvitesComponent()) diff --git a/euth/memberships/emails.py b/euth/memberships/emails.py deleted file mode 100644 index eec296c2a..000000000 --- a/euth/memberships/emails.py +++ /dev/null @@ -1,23 +0,0 @@ -from adhocracy4 import emails - - -class InviteEmail(emails.ExternalNotification): - template_name = 'euth_memberships/emails/invite' - - -class RequestReceivedEmail(emails.ModeratorNotification): - template_name = 'euth_memberships/emails/request_received' - - -class RequestAcceptedEmail( - emails.mixins.SyncEmailMixin, - emails.UserNotification -): - template_name = 'euth_memberships/emails/request_accepted' - - -class RequestDeniedEmail( - emails.mixins.SyncEmailMixin, - emails.UserNotification -): - template_name = 'euth_memberships/emails/request_denied' diff --git a/euth/memberships/forms.py b/euth/memberships/forms.py deleted file mode 100644 index 9d69515b7..000000000 --- a/euth/memberships/forms.py +++ /dev/null @@ -1,103 +0,0 @@ -import re - -from django import forms -from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator -from django.utils.translation import gettext_lazy as _ - -from euth.users.models import User - -from . import models - - -class RequestModerationForm(forms.ModelForm): - - ACTIONS = ( - ('accept', 'Accept'), - ('decline', 'Decline'), - ) - - action = forms.ChoiceField( - choices=ACTIONS, - required=False, - widget=forms.RadioSelect() - ) - - class Meta: - model = models.Request - fields = ['action'] - - -class InviteForm(forms.ModelForm): - accept = forms.CharField(required=False) - reject = forms.CharField(required=False) - - class Meta: - model = models.Invite - fields = ['accept', 'reject'] - - def clean(self): - data = self.data - if 'accept' not in data and 'reject' not in data: - raise ValidationError('Reject or accept') - return data - - def is_accepted(self): - data = self.data - return 'accept' in data and 'reject' not in data - - -class InviteModerationForm(forms.ModelForm): - delete = forms.BooleanField(initial=False, required=False) - - class Meta: - model = models.Invite - fields = ['delete'] - - -class ParticipantsModerationForm(forms.ModelForm): - delete = forms.BooleanField(initial=False, required=False) - - class Meta: - model = User - fields = ['delete'] - - -class ProjectInviteForm(forms.Form): - emails = forms.CharField( - label=_('E-mail addresses of invited users'), - help_text=_('Enter the e-mail addresses of users who you want ' - 'to invite, separated by commas. Invited users will get ' - 'an email to confirm their membership in the project.'), - widget=forms.TextInput(attrs={ - 'placeholder': 'magdalena@example.com, yves@example.com,' - ' nadine@example.com…'} - ), - validators=[RegexValidator( - # a list of emails, separated by commas with optional space after - regex=r'^([^@]+@[^@\s]+\.[^@\s,]+((,\s?)|$))+$', - message=_('Please enter correct e-mail addresses, separated by ' - 'commas.') - )] - ) - - def __init__(self, project, *args, **kwargs): - self.project = project - super().__init__(*args, **kwargs) - - def clean_emails(self): - emails_str = self.cleaned_data['emails'].strip(' ,') - emails = re.split(r',\s?', emails_str) - - query = { - 'project': self.project, - 'email__in': emails, - } - existing = models.Invite.objects.filter(**query) \ - .values_list('email', flat=True) - if existing: - for address in existing: - raise ValidationError( - '{} already invited'.format(address) - ) - return emails diff --git a/euth/memberships/migrations/0003_auto_20240910_1129.py b/euth/memberships/migrations/0003_auto_20240910_1129.py new file mode 100644 index 000000000..26d915148 --- /dev/null +++ b/euth/memberships/migrations/0003_auto_20240910_1129.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.20 on 2024-09-10 09:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_memberships', '0002_add_invitations'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='request', + unique_together=None, + ), + migrations.RemoveField( + model_name='request', + name='creator', + ), + migrations.RemoveField( + model_name='request', + name='project', + ), + migrations.DeleteModel( + name='Invite', + ), + migrations.DeleteModel( + name='Request', + ), + ] diff --git a/euth/memberships/models.py b/euth/memberships/models.py index 158476ea0..e69de29bb 100644 --- a/euth/memberships/models.py +++ b/euth/memberships/models.py @@ -1,86 +0,0 @@ -import uuid - -from django.conf import settings -from django.db import models -from django.urls import reverse - -from adhocracy4.models import base -from adhocracy4.projects import models as prj_models - -from . import emails - - -class InviteManager(models.Manager): - def invite(self, creator, project, email): - invite = super().create(project=project, creator=creator, email=email) - emails.InviteEmail.send(invite) - return invite - - -class Invite(base.TimeStampedModel): - """ - An invite to join a privte project. - """ - creator = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE - ) - project = models.ForeignKey( - prj_models.Project, - on_delete=models.CASCADE - ) - email = models.EmailField() - token = models.UUIDField(default=uuid.uuid4, unique=True) - - objects = InviteManager() - - class Meta: - unique_together = ('email', 'project') - - def __str__(self): - return 'Invite to {s.project} for {s.email}'.format(s=self) - - def get_absolute_url(self): - url_kwargs = {'invite_token': self.token} - return reverse('membership-invite-detail', kwargs=url_kwargs) - - def accept(self, user): - self.project.participants.add(user) - self.delete() - - def reject(self): - self.delete() - - -class RequestManager(models.Manager): - def request_membership(self, project, user): - request = super().create(creator=user, project=project) - emails.RequestReceivedEmail.send(request) - return request - - -class Request(base.UserGeneratedContentModel): - """ - A request for joining a private project. - """ - project = models.ForeignKey( - prj_models.Project, - on_delete=models.CASCADE - ) - - objects = RequestManager() - - class Meta: - unique_together = ('creator', 'project') - - def __str__(self): - return 'Request by {s.creator} for {s.project}'.format(s=self) - - def accept(self): - self.project.participants.add(self.creator) - self.delete() - emails.RequestAcceptedEmail.send(self) - - def decline(self): - self.delete() - emails.RequestDeniedEmail.send(self) diff --git a/euth/memberships/projects_urls.py b/euth/memberships/projects_urls.py deleted file mode 100644 index 2048735c2..000000000 --- a/euth/memberships/projects_urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import re_path - -from . import views - -urlpatterns = [ - re_path(r'^(?P[-\w_]+)/$', views.RequestsProjectDetailView.as_view(), - name='project-detail'), -] diff --git a/euth/memberships/urls.py b/euth/memberships/urls.py deleted file mode 100644 index 1769b3d86..000000000 --- a/euth/memberships/urls.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.urls import re_path - -from . import views - -urlpatterns = [ - re_path( - r'^apply/(?P[-\w_]+)/$', - views.RequestView.as_view(), - name='memberships-request' - ), - - re_path( - r'^invites/(?P[-\w_]+)/$', - views.InviteDetailView.as_view(), - name='membership-invite-detail' - ), - re_path( - r'^invites/(?P[-\w_]+)/accept/$', - views.InviteUpdateView.as_view(), - name='membership-invite-update' - ), -] diff --git a/euth/memberships/views.py b/euth/memberships/views.py deleted file mode 100644 index 6ed48e5aa..000000000 --- a/euth/memberships/views.py +++ /dev/null @@ -1,293 +0,0 @@ -from django import forms -from django.contrib import messages -from django.contrib.auth import mixins as mixin -from django.http import Http404 -from django.shortcuts import redirect -from django.utils.translation import gettext_lazy as _ -from django.views import generic -from rules.contrib import views as rules_views - -from adhocracy4.dashboard import mixins -from adhocracy4.projects import models as prj_models -from adhocracy4.projects.mixins import ProjectMixin -from euth.projects import mixins as prj_mixins -from euth.users import models as user_models - -from . import forms as membership_forms -from . import models as membership_models - - -class InviteDashboardView( - ProjectMixin, - mixins.DashboardBaseMixin, - mixins.DashboardComponentMixin, - generic.detail.SingleObjectMixin, - generic.FormView -): - model = prj_models.Project - fields = [] - permission_required = 'a4projects.change_project' - project_url_kwarg = 'project_slug' - template_name = 'euth_memberships/dashboard_invites.html' - form_class = membership_forms.ProjectInviteForm - - def get_permission_object(self): - return self.project - - def dispatch(self, request, *args, **kwargs): - self.object = self.get_object() - return super().dispatch(request, *args, **kwargs) - - def get_object(self, queryset=None): - return self.project - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['project'] = self.object - return kwargs - - def process_invites(self, invite_formset): - for form in invite_formset: - if form.is_valid(): - data = form.cleaned_data - if 'delete' in data and data['delete']: - form.instance.delete() - - def post(self, request, *args, **kwargs): - if 'submit_action' in request.POST: - if request.POST['submit_action'] == 'remove_invites': - MembershipInviteFormset = forms.modelformset_factory( - membership_models.Invite, - membership_forms.InviteModerationForm, - extra=0) - form_set = MembershipInviteFormset(request.POST) - self.process_invites(form_set) - messages.add_message(request, - messages.SUCCESS, - _('Project member invites ' - 'have been updated')) - response = redirect(self.get_success_url()) - else: - response = super().post(request, *args, **kwargs) - return response - - @property - def formset(self): - membership_invites = \ - membership_models.Invite.objects.filter(project=self.project) - MembershipInviteFormset = forms.modelformset_factory( - membership_models.Invite, - membership_forms.InviteModerationForm, - extra=0) - return MembershipInviteFormset(queryset=membership_invites) - - def form_valid(self, form): - emails = form.cleaned_data['emails'] - user = self.request.user - project = self.object - for email in emails: - membership_models.Invite.objects.invite(user, project, email) - messages.add_message(self.request, - messages.SUCCESS, - _('Invites have been sent')) - return super().form_valid(form) - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - -class MembershipsDashboardView( - ProjectMixin, - mixins.DashboardBaseMixin, - mixins.DashboardComponentMixin, - generic.TemplateView -): - model = prj_models.Project - fields = [] - permission_required = 'a4projects.change_project' - project_url_kwarg = 'project_slug' - template_name = 'euth_memberships/dashboard_members.html' - - def get_permission_object(self): - return self.project - - def get_object(self, queryset=None): - return self.project - - @property - def membership_request_form(self): - membership_requests = \ - membership_models.Request.objects.filter(project=self.project) - MembershipRequestFormset = forms.modelformset_factory( - membership_models.Request, - membership_forms.RequestModerationForm, - extra=0) - return MembershipRequestFormset(queryset=membership_requests) - - @property - def member_delete_form(self): - members = self.project.participants.all() - MemberFormset = forms.modelformset_factory( - user_models.User, - membership_forms.ParticipantsModerationForm, - extra=0) - return MemberFormset(queryset=members) - - def process_requests(self, data): - MembershipRequestFormset = forms.modelformset_factory( - membership_models.Request, - membership_forms.RequestModerationForm, - extra=0) - for form in MembershipRequestFormset(data): - if form.is_valid(): - if form.cleaned_data['action'] == 'accept': - form.instance.accept() - if form.cleaned_data['action'] == 'decline': - form.instance.decline() - - def process_members(self, data): - MemberFormset = forms.modelformset_factory( - user_models.User, - membership_forms.ParticipantsModerationForm, - extra=0) - for form in MemberFormset(data): - if form.is_valid(): - data = form.cleaned_data - if 'delete' in data and data['delete']: - self.project.participants.remove(form.instance) - - def post(self, request, *args, **kwargs): - if 'submit_action' in request.POST: - if request.POST['submit_action'] == 'remove_members': - self.process_members(request.POST) - messages.add_message(self.request, - messages.SUCCESS, - _('Project membership requests' - ' have been updated')) - if request.POST['submit_action'] == 'update_request': - self.process_requests(request.POST) - messages.add_message(request, - messages.SUCCESS, - _('Project membership requests' - ' have been updated')) - response = redirect(self.get_success_url()) - else: - response = super().post(request, *args, **kwargs) - return response - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - -class RequestsProjectDetailView( - rules_views.PermissionRequiredMixin, - prj_mixins.PhaseDispatchMixin, - generic.DetailView -): - model = prj_models.Project - permission_required = 'a4projects.view_project' - project_url_kwarg = 'slug' - - @property - def raise_exception(self): - return self.request.user.is_authenticated - - @property - def project(self): - """ - Emulate ProjectMixin interface for template sharing. - """ - return self.get_object() - - def handle_no_permission(self): - """ - Check if user could join - """ - user = self.request.user - is_member = user.is_authenticated and self.project.has_member(user) - - if is_member: - return super().handle_no_permission() - else: - return self.handle_no_membership() - - def handle_no_membership(self): - membership_impossible = ( - not self.request.user.is_authenticated - or self.project.is_draft - or self.project.has_member(self.request.user) - ) - - if membership_impossible: - return super().handle_no_permission() - else: - return redirect('memberships-request', - project_slug=self.project.slug) - - -class InviteDetailView(generic.DetailView): - model = membership_models.Invite - slug_field = 'token' - slug_url_kwarg = 'invite_token' - - def dispatch(self, request, invite_token, *args, **kwargs): - if request.user.is_authenticated: - return redirect( - 'membership-invite-update', - invite_token=invite_token - ) - else: - return super().dispatch(request, *args, **kwargs) - - -class InviteUpdateView(mixin.LoginRequiredMixin, generic.UpdateView): - model = membership_models.Invite - form_class = membership_forms.InviteForm - slug_field = 'token' - slug_url_kwarg = 'invite_token' - - def form_valid(self, form): - if form.is_accepted(): - form.instance.accept(self.request.user) - return redirect(form.instance.project.get_absolute_url()) - else: - form.instance.reject() - return redirect('/') - - -class RequestView(mixin.LoginRequiredMixin, generic.DetailView): - """ - Displays membership request if it exists or allows to create one. - """ - model = membership_models.Request - slug_field = 'project__slug' - slug_url_kwarg = 'project_slug' - context_object_name = 'join_request' - - def get_queryset(self): - return self.model.objects.filter(creator=self.request.user) - - def get(self, request, *args, **kwargs): - if self.project.has_member(request.user): - return redirect(self.project.get_absolute_url()) - else: - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - user = request.user - project = self.project - membership_models.Request.objects.request_membership(project, user) - return redirect(self.request.path) - - def get_object(self, queryset=None): - try: - return super().get_object(queryset) - except Http404: - return None - - @property - def project(self): - project_slug = self.kwargs[self.slug_url_kwarg] - return prj_models.Project.objects.get(slug=project_slug) diff --git a/euth/offlinephases/admin.py b/euth/offlinephases/admin.py deleted file mode 100644 index ceb025f53..000000000 --- a/euth/offlinephases/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin - -from . import models - -admin.site.register(models.Offlinephase) -admin.site.register(models.FileUpload) -admin.site.register(models.OfflineEvent) -admin.site.register(models.OfflineEventFileUpload) diff --git a/euth/offlinephases/apps.py b/euth/offlinephases/apps.py index 3253fe242..01146b767 100644 --- a/euth/offlinephases/apps.py +++ b/euth/offlinephases/apps.py @@ -4,6 +4,3 @@ class Config(AppConfig): name = 'euth.offlinephases' label = 'euth_offlinephases' - - def ready(self): - import euth.offlinephases.signals # noqa:F401 diff --git a/euth/offlinephases/dashboard.py b/euth/offlinephases/dashboard.py deleted file mode 100644 index bd7dbedec..000000000 --- a/euth/offlinephases/dashboard.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.dashboard import DashboardComponent -from adhocracy4.dashboard import components - -from . import views - - -class OfflineEventsComponent(DashboardComponent): - identifier = 'offlineevents' - weight = 20 - label = _('Offline Events') - - def is_effective(self, project): - return True - - def get_progress(self, project): - return 0, 0 - - def get_base_url(self, project): - return reverse('a4dashboard:offlineevent-list', kwargs={ - 'project_slug': project.slug - }) - - def get_urls(self): - return [ - (r'^offlineevents/projects/(?P[-\w_]+)/$', - views.OfflineEventListView.as_view(component=self), - 'offlineevent-list'), - (r'^offlineevents/create/project/(?P[-\w_]+)/$', - views.OfflineEventCreateView.as_view(component=self), - 'offlineevent-create'), - (r'^offlineevents/(?P[-\w_]+)/update/$', - views.OfflineEventUpdateView.as_view(component=self), - 'offlineevent-update'), - (r'^offlineevents/(?P[-\w_]+)/delete/$', - views.OfflineEventDeleteView.as_view(component=self), - 'offlineevent-delete') - ] - - -components.register_project(OfflineEventsComponent()) diff --git a/euth/offlinephases/forms.py b/euth/offlinephases/forms.py deleted file mode 100644 index 1554e8056..000000000 --- a/euth/offlinephases/forms.py +++ /dev/null @@ -1,31 +0,0 @@ -from django import forms -from django.utils.translation import gettext_lazy as _ - -from adhocracy4.forms.fields import DateTimeField -from euth.contrib import widgets - -from . import models - - -class OfflineEventForm(forms.ModelForm): - - date = DateTimeField( - time_format='%H:%M', - required=True, - require_all_fields=False, - label=(_('Date'), _('Time')) - ) - - class Meta: - model = models.OfflineEvent - fields = ['name', 'date', 'description'] - - -class FileUploadForm(forms.ModelForm): - - class Meta: - model = models.OfflineEventFileUpload - fields = ['title', 'document'] - widgets = { - 'document': widgets.FileUploadWidget() - } diff --git a/euth/offlinephases/migrations/0007_auto_20240910_1555.py b/euth/offlinephases/migrations/0007_auto_20240910_1555.py new file mode 100644 index 000000000..ae9e4b0c4 --- /dev/null +++ b/euth/offlinephases/migrations/0007_auto_20240910_1555.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.20 on 2024-09-10 13:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_offlinephases', '0006_alter_offlineevent_file_upload'), + ] + + operations = [ + migrations.RemoveField( + model_name='offlineeventfileupload', + name='offlineevent', + ), + migrations.DeleteModel( + name='OfflineEvent', + ), + migrations.DeleteModel( + name='OfflineEventFileUpload', + ), + ] diff --git a/euth/offlinephases/migrations/0008_auto_20240910_1557.py b/euth/offlinephases/migrations/0008_auto_20240910_1557.py new file mode 100644 index 000000000..1a0d18cf5 --- /dev/null +++ b/euth/offlinephases/migrations/0008_auto_20240910_1557.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.20 on 2024-09-10 13:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('euth_offlinephases', '0007_auto_20240910_1555'), + ] + + operations = [ + migrations.RemoveField( + model_name='offlinephase', + name='phase', + ), + migrations.DeleteModel( + name='FileUpload', + ), + migrations.DeleteModel( + name='Offlinephase', + ), + ] diff --git a/euth/offlinephases/models.py b/euth/offlinephases/models.py index 4c07a0732..8549274d2 100644 --- a/euth/offlinephases/models.py +++ b/euth/offlinephases/models.py @@ -1,94 +1,3 @@ -from autoslug import AutoSlugField -from ckeditor_uploader.fields import RichTextUploadingField -from django.contrib.contenttypes.fields import GenericRelation -from django.db import models -from django.utils import functional -from django.utils.translation import gettext_lazy as _ - -from adhocracy4 import transforms -from adhocracy4.comments import models as comment_models -from adhocracy4.models import base -from adhocracy4.phases import models as phase_models -from adhocracy4.projects import models as project_models -from euth.contrib import validators - - -class Offlinephase(base.TimeStampedModel): - text = RichTextUploadingField(blank=True, config_name='image-editor',) - phase = models.OneToOneField( - phase_models.Phase, - on_delete=models.CASCADE, - primary_key=True, - related_name='offlinephase' - ) - comments = GenericRelation(comment_models.Comment, - related_query_name='offlinephase', - object_id_field='object_pk') - - def __str__(self): - return "{}_offlinephase_{}".format(str(self.phase), self.pk) - - def save(self, *args, **kwargs): - self.text = transforms.clean_html_field( - self.text, 'image-editor') - super().save(*args, **kwargs) - - @functional.cached_property - def project(self): - return self.phase.module.project - - @functional.cached_property - def module(self): - return self.phase.module - - @functional.cached_property - def organisation(self): - return self.project.organisation - - -class OfflineEvent(base.TimeStampedModel): - slug = AutoSlugField(populate_from='name', unique=True) - name = models.CharField(max_length=120, verbose_name=_('Title')) - date = models.DateTimeField(verbose_name=_('Date')) - description = RichTextUploadingField( - config_name='image-editor', verbose_name=_('Description')) - project = models.ForeignKey( - project_models.Project, on_delete=models.CASCADE) - - class Meta: - ordering = ['-date'] - - def __str__(self): - return self.name - - def save(self, *args, **kwargs): - self.description = transforms.clean_html_field( - self.description, 'image-editor') - super().save(*args, **kwargs) - - def document_path(instance, filename): return 'documents/offlineevent_{}/{}'.format( instance.offlineevent.pk, filename) - - -class FileUpload(base.TimeStampedModel): - title = models.CharField(max_length=256) - document = models.FileField( - upload_to=document_path, - validators=[validators.validate_file_type_and_size]) - offlinephase = models.ForeignKey( - Offlinephase, - on_delete=models.CASCADE - ) - - -class OfflineEventFileUpload(base.TimeStampedModel): - title = models.CharField(max_length=256) - document = models.FileField( - upload_to='offlineevents/documents', - validators=[validators.validate_file_type_and_size]) - offlineevent = models.ForeignKey( - OfflineEvent, - on_delete=models.CASCADE - ) diff --git a/euth/offlinephases/signals.py b/euth/offlinephases/signals.py deleted file mode 100644 index 4bad5420e..000000000 --- a/euth/offlinephases/signals.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db.models.signals import post_delete -from django.db.models.signals import post_init -from django.db.models.signals import post_save -from django.dispatch import receiver - -from .models import OfflineEventFileUpload - - -@receiver(post_init, sender=OfflineEventFileUpload) -def backup_file_path(sender, instance, **kwargs): - instance._current_document_file = instance.document - - -@receiver(post_save, sender=OfflineEventFileUpload) -def delete_old_fileuploads(sender, instance, **kwargs): - if hasattr(instance, '_current_document_file'): - if instance._current_document_file != instance.document: - instance._current_document_file.delete(False) - - -@receiver(post_delete, sender=OfflineEventFileUpload) -def delete_documents_for_Fileupload(sender, instance, **kwargs): - instance.document.delete(False) diff --git a/euth/offlinephases/urls.py b/euth/offlinephases/urls.py deleted file mode 100644 index 35f0a0d30..000000000 --- a/euth/offlinephases/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path - -from . import views - -urlpatterns = [ - path('/', views.OfflineEventDetailView.as_view(), - name='offlineevent-detail'), -] diff --git a/euth/offlinephases/views.py b/euth/offlinephases/views.py deleted file mode 100644 index 41c25ac4f..000000000 --- a/euth/offlinephases/views.py +++ /dev/null @@ -1,190 +0,0 @@ -from django.contrib import messages -from django.db import transaction -from django.shortcuts import redirect -from django.shortcuts import render -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ -from django.views import generic -from rules.contrib.views import PermissionRequiredMixin - -from adhocracy4.dashboard import mixins -from adhocracy4.projects.mixins import ProjectMixin - -from . import forms -from . import models -from .mixins import OfflineEventFormMixin - - -class OfflineEventDetailView(PermissionRequiredMixin, - generic.DetailView): - model = models.OfflineEvent - permission_required = 'euth_offlinephases.view_offlineevent' - - @property - def project(self): - return self.object.project - - -class OfflineEventListView(ProjectMixin, - mixins.DashboardBaseMixin, - mixins.DashboardComponentMixin, - generic.ListView): - - model = models.OfflineEvent - template_name = 'euth_offlinephases/offlineevent_list.html' - permission_required = 'a4projects.change_project' - - def get_queryset(self): - return super().get_queryset().filter(project=self.project) - - def get_permission_object(self): - return self.project - - -class OfflineEventCreateView( - ProjectMixin, - mixins.DashboardBaseMixin, - mixins.DashboardComponentMixin, - generic.TemplateView, - OfflineEventFormMixin -): - template_name = 'euth_offlinephases/offlineevent_form.html' - permission_required = 'a4projects.change_project' - project_url_kwarg = 'project_slug' - - def get_permission_object(self): - return self.project - - def get_success_url(self): - return reverse( - 'a4dashboard:offlineevent-list', - kwargs={'project_slug': self.project.slug}) - - def get_context_data(self, form=None, upload_forms=None, **kwargs): - context = super().get_context_data(**kwargs) - if not form: - form = forms.OfflineEventForm() - if not upload_forms: - upload_forms = self.empty_upload_formset() - context['form'] = form - context['upload_forms'] = upload_forms - return context - - def _process_formdata(self, form, upload_forms): - form.instance.project = self.project - with transaction.atomic(): - object = form.save() - instances = upload_forms.save(commit=False) - for instance in instances: - instance.offlineevent = object - instance.save() - - def post(self, request, *args, **kwargs): - form = forms.OfflineEventForm(request.POST) - upload_forms = self.filled_upload_formset(request) - if form.is_valid() and upload_forms.is_valid(): - self._process_formdata(form, upload_forms) - messages.add_message(request, - messages.SUCCESS, - _('Offline events ' - 'have been updated')) - response = redirect(self.get_success_url()) - else: - response = render(request, - self.template_name, - self.get_context_data(form=form, - upload_forms=upload_forms)) - return response - - -class OfflineEventUpdateView(ProjectMixin, - mixins.DashboardBaseMixin, - mixins.DashboardComponentMixin, - generic.detail.SingleObjectMixin, - generic.TemplateView, - OfflineEventFormMixin): - - model = models.OfflineEvent - permission_required = 'a4projects.change_project' - template_name = 'euth_offlinephases/offlineevent_form.html' - get_context_from_object = True - - def dispatch(self, *args, **kwargs): - self.object = self.get_object() - return super().dispatch(*args, **kwargs) - - def get_context_data(self, form=None, upload_forms=None, **kwargs): - context = super().get_context_data(**kwargs) - if not form: - form = forms.OfflineEventForm(instance=self.get_object()) - if not upload_forms: - queryset = \ - models.OfflineEventFileUpload\ - .objects.filter(offlineevent=self.get_object()) - upload_forms = self.update_upload_formset(queryset) - context['form'] = form - context['upload_forms'] = upload_forms - return context - - def get_success_url(self): - return reverse( - 'a4dashboard:offlineevent-list', - kwargs={'project_slug': self.project.slug}) - - def get_permission_object(self): - return self.project - - def _process_formdata(self, form, upload_forms): - with transaction.atomic(): - form.save() - instances = upload_forms.save(commit=False) - for obj in upload_forms.deleted_objects: - obj.delete() - for instance in instances: - instance.offlineevent = self.object - instance.save() - - def post(self, request, *args, **kwargs): - upload_forms = self.filled_upload_formset(request) - form = forms.OfflineEventForm(request.POST, instance=self.object) - if upload_forms.is_valid() and form.is_valid(): - self._process_formdata(form, upload_forms) - messages.add_message(request, - messages.SUCCESS, - _('Offline events ' - 'have been updated')) - response = redirect(self.get_success_url()) - else: - response = render(request, - self.template_name, - self.get_context_data( - form=form, upload_forms=upload_forms)) - return response - - -class OfflineEventDeleteView(ProjectMixin, - mixins.DashboardBaseMixin, - mixins.DashboardComponentMixin, - mixins.DashboardComponentDeleteSignalMixin, - generic.DeleteView): - model = models.OfflineEvent - success_message = _('The offline event has been deleted') - permission_required = 'a4projects.change_project' - template_name = 'euth_offlinephases/offlineevent_confirm_delete.html' - get_context_from_object = True - - def delete(self, request, *args, **kwargs): - messages.success(self.request, self.success_message) - return super().delete(request, *args, **kwargs) - - def get_success_url(self): - return reverse( - 'a4dashboard:offlineevent-list', - kwargs={'project_slug': self.project.slug}) - - @property - def organisation(self): - return self.project.organisation - - def get_permission_object(self): - return self.project diff --git a/euth/organisations/templates/euth_organisations/organisation_list.html b/euth/organisations/templates/euth_organisations/organisation_list.html index 6805ac593..3a5e370d4 100644 --- a/euth/organisations/templates/euth_organisations/organisation_list.html +++ b/euth/organisations/templates/euth_organisations/organisation_list.html @@ -9,9 +9,6 @@

{% trans 'Take part in a project' %}