Skip to main content
  • Home
  • Development
  • Documentation
  • Donate
  • Operational login
  • Browse the archive

swh logo
SoftwareHeritage
Software
Heritage
Archive
Features
  • Search

  • Downloads

  • Save code now

  • Add forge now

  • Help

https://github.com/planetary-research/signatories
07 March 2026, 08:16:53 UTC
  • Code
  • Branches (2)
  • Releases (0)
  • Visits
    • Branches
    • Releases
    • HEAD
    • refs/heads/main
    • refs/tags/v1.0
    No releases to show
  • e94d917
  • /
  • app.py
Raw File Download Save again
Take a new snapshot of a software origin

If the archived software origin currently browsed is not synchronized with its upstream version (for instance when new commits have been issued), you can explicitly request Software Heritage to take a new snapshot of it.

Use the form below to proceed. Once a request has been submitted and accepted, it will be processed as soon as possible. You can then check its processing state by visiting this dedicated page.
swh spinner

Processing "take a new snapshot" request ...

To reference or cite the objects present in the Software Heritage archive, permalinks based on SoftWare Hash IDentifiers (SWHIDs) must be used.
Select below a type of object currently browsed in order to display its associated SWHID and permalink.

  • content
  • directory
  • revision
  • snapshot
origin badgecontent badge
swh:1:cnt:e281a512ec70ac11853cdf8c98cabf7c713ede10
origin badgedirectory badge
swh:1:dir:e94d91701888d2cae32c6332face19f00dd81b56
origin badgerevision badge
swh:1:rev:e0f89ed883ffc47241a4c62503ce6e4784463cea
origin badgesnapshot badge
swh:1:snp:9fad922ad80f6485c66f4c22a7580dda3f157802

This interface enables to generate software citations, provided that the root directory of browsed objects contains a citation.cff or codemeta.json file.
Select below a type of object currently browsed in order to generate citations for them.

  • content
  • directory
  • revision
  • snapshot
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Generate software citation in BibTex format (requires biblatex-software package)
Generating citation ...
Tip revision: e0f89ed883ffc47241a4c62503ce6e4784463cea authored by markwieczorek on 14 February 2026, 22:25:20 UTC
Merge pull request #41 from MarkWieczorek/main
Tip revision: e0f89ed
app.py
import os
import re
import datetime
from datetime import timedelta
from io import BytesIO
import tomllib
from flask import Flask
from flask import request, session
from flask import redirect, render_template
from flask import send_from_directory, send_file
from markupsafe import escape
from waitress import serve
import orcid
from pyexcel_ods3 import save_data
from feedgen.feed import FeedGenerator

import config
from db_models import db, Signatory, Admin, Campaign, UserRole, Block
from utils import get_orcid_name, checksum


""" ORCID API """
if config.orcid_member:
    api = orcid.MemberAPI(config.client_ID, config.client_secret, sandbox=config.sandbox)
else:
    api = orcid.PublicAPI(config.client_ID, config.client_secret, sandbox=config.sandbox)

if config.sandbox:
    api._token_url = "https://sandbox.orcid.org/oauth/token"
else:
    api._token_url = "https://orcid.org/oauth/token"

""" App configuration """
app = Flask(__name__)

app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = config.db_URI
app.config["SECRET_KEY"] = config.cookie_secret
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=2)
app.config.from_object(__name__)

""" Database """
db.init_app(app)

# Create database if it doesn't exist and add admin
if not os.path.exists(config.dbpath):
    print(f"Database doesn't exist. Creating new database: {config.db_URI}")
    if not os.path.isdir(config.dbdir):
        os.mkdir(config.dbdir)
    with app.app_context():
        db.create_all()
        # get admin name from orcid
        name = get_orcid_name(api, config.admin_orcid)
        admin = Admin(orcid=config.admin_orcid, name=name, role_id=3)
        db.session.add(admin)
        db.session.commit()

        for role_name in ("User", "Editor", "Administrator"):
            role = UserRole(name=role_name)
            db.session.add(role)

        db.session.commit()

""" Read files in Campaigns and add to database if they don't already exist """
files = os.listdir(config.campaigndir)

reserved_actions = [
    "logout",
    "privacy",
    "faq",
    "admin",
    "insufficient-privileges",
    "create",
    "editor",
    "user-banned",
    "feed",
    "feeds",
]

for file in files:
    with open(os.path.join(config.campaigndir, file), "rb") as f:
        data = tomllib.load(f)
        with app.app_context():
            result = Campaign.query.filter_by(action_slug=data["ACTION_SLUG"]).first()
            reserved_actions.append(data["ACTION_SLUG"])
            if not result:
                print(f"Creating new campaing for file: {file}")
                new_campaign = Campaign(
                    action_slug=data["ACTION_SLUG"],
                    action_kind=data["ACTION_KIND"],
                    action_name=data["ACTION_NAME"],
                    action_short_description=data["ACTION_SHORT_DESCRIPTION"],
                    action_text=data["ACTION_TEXT"],
                    sort_alphabetical=data["SORT_ALPHABETICAL"],
                    allow_anonymous=data["ALLOW_ANONYMOUS"],
                )
                db.session.add(new_campaign)
                db.session.commit()

""" Update database for any new tables """
with app.app_context():
    db.create_all()
    db.session.commit()

""" Default URLs """

home_URI = config.site_path
logout_URI = os.path.join(config.site_path, "logout")
user_URI = os.path.join(config.site_path, "<slug>", "user")
thank_you_URI = os.path.join(config.site_path, "<slug>", "thank-you")
signature_removed_URI = os.path.join(config.site_path, "<slug>", "signature-removed")
privacy_URI = os.path.join(config.site_path, "privacy")
faq_URI = os.path.join(config.site_path, "faq")
action_URI = os.path.join(config.site_path, "<slug>")
admin_URI = os.path.join(config.site_path, "admin")
insufficient_privileges_URI = os.path.join(config.site_path, "insufficient-privileges")
create_URI = os.path.join(config.site_path, "create")
editor_URI = os.path.join(config.site_path, "editor")
edit_URI = os.path.join(config.site_path, "<slug>", "edit")
banned_URI = os.path.join(config.site_path, "user-banned")

action_template = "action-with-sidebar.html"  # default template for actions

base_data = {
    "home_uri": home_URI,
    "logout_uri": logout_URI,
    "user_uri": user_URI,
    "privacy_uri": privacy_URI,
    "faq_uri": faq_URI,
    "thank_you_uri": thank_you_URI,
    "signature_removed_URI": signature_removed_URI,
    "admin_uri": admin_URI,
    "create_uri": create_URI,
    "editor_uri": editor_URI,
    "user_URI_defined": None,
    "thank_you_URI_defined": None,
    "signature_removed_URI_defined": None,
    "signatories_url": config.signatories_url,
    "footer_url_name": config.footer_url_name,
    "footer_url": config.footer_url,
    "show_examples": config.show_examples,
    "thank_prc": config.thank_prc,
    "contact_email": config.contact_email,
    "orcid_url": config.orcid_url,
    "authorization_uri_admin": api.get_login_url(
        scope="/authenticate",
        redirect_uri=config.code_callback_URI + "-admin"),
    "redirect_alerts": None,
    "role_id": 0,
    "everyone_is_editor": config.everyone_is_editor,
}

base_alerts = {
    "success": None,
    "danger": None,
    "info": None,
    "warning": None,
}


""" Routes """


@app.route('/favicon.ico')
def favicon():
    return send_from_directory(
        os.path.join(app.root_path, 'static/img'),
        config.favicon, mimetype='image/vnd.microsoft.icon')


@app.route(home_URI)
def home():
    # Home page
    if session.get("orcid") is None:
        role_id = 0
    elif base_data["everyone_is_editor"] is True:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            role_id = 0
        else:
            role_id = user.role_id

    campaign_list = dict()
    # Create list of signatory campaigns
    for row in Campaign.query.filter_by(is_active=True).order_by(Campaign.creation_date.desc()).all():
        campaign_list[row.action_slug] = [
            row.action_name, row.action_short_description,
            os.path.join(config.site_path, row.action_slug)]
    data = {
        "header_title": config.site_title,
        "header_subtitle": config.site_subtitle,
        "header_path": config.site_path,
        "text_header": config.site_header,
        "campaigns": campaign_list,
        "page": "home",
        "role_id": role_id,
    }
    base_data["user_URI_defined"] = None
    base_data["thank_you_URI_defined"] = None
    base_data["signature_removed_URI_defined"] = None

    return render_template("index.html", **(base_data | data))


@app.route(action_URI, methods=["POST", "GET"])
def action(slug):
    # Show the campaign
    if session.get("orcid") is None:
        role_id = 0
    elif base_data["everyone_is_editor"] is True:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            role_id = 0
        else:
            role_id = user.role_id

    # Get the ORCID authentication URI
    URI = api.get_login_url(scope="/authenticate", redirect_uri=config.code_callback_URI)

    # check if the campaign exists
    result = Campaign.query.filter_by(action_slug=slug).first()
    if not result:
        data = {
            "header_title": config.site_title,
            "header_subtitle": config.site_subtitle,
            "header_path": config.site_path,
        }
        return render_template("campaign-not-found.html", **(base_data | data))

    action_data = Campaign.query.filter_by(action_slug=slug).first()

    # Create list of signatories and counts
    total_signatures = len(Signatory.query.filter_by(campaign=slug).all())
    anonymous_signatures = len(Signatory.query.filter_by(anonymous=True, campaign=slug).all())
    if action_data.sort_alphabetical:
        visible_signatures = Signatory.query.filter_by(anonymous=False, campaign=slug).order_by(Signatory.name.asc()).all()
    else:
        visible_signatures = Signatory.query.filter_by(anonymous=False, campaign=slug).all()

    if request.method == "POST":
        if request.form.get("mode") == "download-ods":
            visible_signatures_list = []
            for row in visible_signatures:
                if row.affiliation is None:
                    affiliation = ''
                else:
                    affiliation = row.affiliation

                visible_signatures_list.append([row.name, affiliation, row.orcid, 'https://orcid.org/'+row.orcid])
            ods_output = {slug: visible_signatures_list}
            ods_bytes = BytesIO()
            save_data(ods_bytes, ods_output)
            ods_bytes.seek(0)  # Reset the pointer to the beginning of the file
            return send_file(ods_bytes, as_attachment=True, download_name=slug+".ods")

    data = {
        "header_title": action_data.action_name,
        "header_subtitle": action_data.action_kind.upper(),
        "header_path": os.path.join(config.site_path, slug),
        "action_kind": action_data.action_kind,
        "text_header": action_data.action_short_description,
        "action_text": action_data.action_text,
        "action_created": action_data.creation_date,
        "action_closed": action_data.closed_date,
        "authorization_uri": URI,
        "total_signatures": total_signatures,
        "anonymous_signatures": anonymous_signatures,
        "visible_signatures": visible_signatures,
        "is_active": action_data.is_active,
        "allow_anonymous": action_data.allow_anonymous,
        "role_id": role_id,
        "download_uri": os.path.join(config.site_path, slug),
    }
    base_data["user_URI_defined"] = os.path.join(config.site_path, slug, "user")
    base_data["thank_you_URI_defined"] = os.path.join(config.site_path, slug, "thank-you")
    base_data["signature_removed_URI_defined"] = os.path.join(config.site_path, slug, "signature-removed")

    return render_template(action_template, **(base_data | data))


@app.route("/authorization-code-callback", methods=["GET"])
def authorize():
    # Instantiate the return code
    code = None

    # If a GET request is made
    if request.method == "GET":
        # Fetch (and sanitise) the return code
        code = escape(request.args["code"])

        # Exchange the security code for a token
        token = api.get_token_from_authorization_code(code, config.code_callback_URI)

        # Extract the ORCID and user name from the token, and set to session
        session["orcid"] = escape(token["orcid"])
        session["name"] = escape(token["name"])
        session.permanent = True

        # check if user is banned
        if len(Block.query.filter_by(orcid=session["orcid"]).all()) > 0:
            return redirect(banned_URI)

        # Serve the user page
        return redirect(base_data["user_URI_defined"])

    return "Fetching ORCID account details..."


@app.route("/authorization-code-callback-admin", methods=["GET"])
def authorize_admin():
    # Instantiate the return code
    code = None

    # If a GET request is made
    if request.method == "GET":
        # Fetch (and sanitise) the return code
        code = escape(request.args["code"])

        # Exchange the security code for a token
        token = api.get_token_from_authorization_code(code, config.code_callback_URI+"-admin")

        # Extract the ORCID and user name from the token, and set to session
        session["orcid"] = escape(token["orcid"])
        session["name"] = escape(token["name"])
        session.permanent = True

        # check if user is banned
        if len(Block.query.filter_by(orcid=session["orcid"]).all()) > 0:
            return redirect(banned_URI)

        return redirect(editor_URI)

    return "Fetching ORCID account details..."


@app.route(privacy_URI)
def privacy():
    # Show the privacy page
    if session.get("orcid") is None:
        role_id = 0
    elif base_data["everyone_is_editor"] is True:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            role_id = 0
        else:
            role_id = user.role_id

    data = {
        "header_title": config.site_title,
        "header_subtitle": config.site_subtitle,
        "header_path": config.site_path,
        "role_id": role_id,
    }
    return render_template("privacy.html", **(base_data | data))


@app.route(faq_URI)
def faq():
    # Show the faq page
    if session.get("orcid") is None:
        role_id = 0
    elif base_data["everyone_is_editor"] is True:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            role_id = 0
        else:
            role_id = user.role_id

    data = {
        "header_title": config.site_title,
        "header_subtitle": config.site_subtitle,
        "header_path": config.site_path,
        "role_id": role_id,
    }
    return render_template("faq.html", **(base_data | data))


@app.route(user_URI, methods=["POST", "GET"])
def user(slug):
    # Show the page allowing a logged in user to sign a campaign
    if session.get("orcid") is None:
        return redirect(home_URI)
    elif base_data["everyone_is_editor"] is True:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            role_id = 0
        else:
            role_id = user.role_id

    user = Signatory.query.filter_by(orcid=session["orcid"], campaign=slug).first()
    action_data = Campaign.query.filter_by(action_slug=slug).first()

    # Default alerts
    alerts = base_alerts.copy()

    # Execute when an update is pushed
    if request.method == "POST":
        # Update signature information
        if request.form.get("mode") == "update_info":
            affiliation = request.form["affiliation"]
            anonymous = request.form["anonymous"]

            # The user is not yet in the database
            if user is None:
                user = Signatory(
                    orcid=session["orcid"], name=session["name"], campaign=slug)
                db.session.add(user)
                db.session.commit()

            user.affiliation = affiliation
            if anonymous == "True":
                user.anonymous = True
            else:
                user.anonymous = False

            db.session.commit()

            return redirect(base_data["thank_you_URI_defined"])

        # Delete all user data
        if request.form.get("mode") == "delete":
            # Check the confirmation option
            if request.form["confirmation"].lower() == "delete":
                # Delete user account
                Signatory.query.filter_by(orcid=session["orcid"], campaign=slug).delete()
                # Commit to database
                db.session.commit()
                # Logout
                return redirect(base_data["signature_removed_URI_defined"])
            else:
                alerts["danger"] = "Please confirm your response with \"delete\""

    if user is not None:
        in_database = True
        affiliation = user.affiliation
        anonymous = user.anonymous
        if user.name == '':
            alerts["danger"] = "Your ORCID user name is marked as private and will not be shown. " \
                "Please change the visibility of your name in your ORCID account."
    else:
        in_database = False
        affiliation = ''
        anonymous = None

    data = {
        "header_path": os.path.join(config.site_path, slug),
        "header_title": action_data.action_name,
        "header_subtitle": action_data.action_short_description,
        "action_kind": action_data.action_kind,
        "action_text": action_data.action_text,
        "name": session["name"],
        "affiliation": affiliation,
        "anonymous": anonymous,
        "allow_anonymous": action_data.allow_anonymous,
        "orcid_id": session["orcid"],
        "alert": alerts,
        "in_database": in_database,
        "role_id": role_id,
        "page": "user",
    }

    return render_template("user.html", **(base_data | data))


@app.route(admin_URI, methods=["POST", "GET"])
def admin():
    # Show the admin page

    # Check if the user is logged in
    if session.get("orcid") is None:
        return redirect(home_URI)

    # Query database for user's ORCID
    user = Admin.query.filter_by(orcid=session["orcid"]).first()
    if user is None:
        print("User is not in the Admin database")
        return redirect(insufficient_privileges_URI)

    # Check if the user has sufficient permissions
    if user.role_id < 3:
        print("Insufficient permissions to view the Admin page")
        return redirect(insufficient_privileges_URI)

    role = UserRole.query.filter_by(role_id=user.role_id).first()
    modify_options = [[1, "Remove"], [2, "Editor"], [3, "Administrator"]]
    delete_options = [[1, "Delete"], [2, "Ban"], [3, "Remove ban"]]
    alerts = base_alerts.copy()

    orphans = len(Campaign.query.filter_by(action_slug='').all())

    # If an update is pushed
    if request.method == "POST":

        # Add or modify a user
        if request.form.get("mode") == "modify_user":
            # Get the user's ORCID
            user_id = escape(request.form["user_id"])
            # Get the desired user role
            role_id = int(request.form["user_role"])

            # Check if we are not accidently changing self
            if user_id == session["orcid"]:
                alerts["danger"] = "You cannot modify yourself."
            # Check if the ORCID is valid (4 groups of 4 digits)
            elif (re.match(r"\d{4}-\d{4}-\d{4}-\d{3}[0-9|xX]", user_id.strip()) is None) or not checksum(user_id.strip()):
                alerts["danger"] = "Invalid ORCID."
            # All good
            else:
                # Try to get user from DB
                user = Admin.query.filter_by(orcid=user_id).first()
                if user is None and role_id > 1:
                    # Try to get public name and email from orcid profile
                    orcid_name = get_orcid_name(api, user_id)
                    if orcid_name == '':
                        alerts["warning"] = "The ORCID user name is marked as private and will not be shown."
                    # Add new user
                    user = Admin(orcid=user_id, name=orcid_name, role_id=role_id)
                    db.session.add(user)
                    alerts["success"] = "New user added to admin database."
                elif user is None and role_id == 1:
                    alerts["warning"] = "User does not exist and can not be deleted."
                elif role_id > 1:
                    # Modify role ID
                    if user.role_id == role_id:
                        alerts["info"] = "User role did not need to be modified."
                    else:
                        user.role_id = role_id
                        alerts["success"] = "User role modified."
                else:
                    db.session.delete(user)
                    alerts["success"] = "User deleted."

                db.session.commit()

        # Delete or ban user
        if request.form.get("mode") == "delete_ban_user":
            # Get the user's ORCID
            user_id = escape(request.form["user_id"])
            # Get the desired user role
            user_option = int(request.form["user_option"])

            # Check if we are not accidently changing self
            if user_id == session["orcid"]:
                alerts["danger"] = "You cannot delete, ban, or unban your own account."
            # Check if the ORCID is valid (4 groups of 4 digits)
            elif (re.match(r"\d{4}-\d{4}-\d{4}-\d{3}[0-9|xX]", user_id.strip()) is None) or not checksum(user_id.strip()):
                alerts["danger"] = "Invalid ORCID iD."
            elif Admin.query.filter_by(orcid=user_id).first() is not None:
                alerts["danger"] = "Can not delete, ban or unban users with administrator roles."
            else:
                if user_option == 1:
                    result = Signatory.query.filter_by(orcid=user_id).all()
                    num_deleted = len(result)
                    if num_deleted > 0:
                        Signatory.query.filter_by(orcid=user_id).delete()
                        db.session.commit()
                        if num_deleted == 1:
                            alerts["success"] = f"Deleted {num_deleted} signature associated with ORCID iD {user_id}."
                        else:
                            alerts["success"] = f"Deleted {num_deleted} signatures associated with ORCID iD {user_id}."
                    else:
                        alerts["info"] = f"No signatures to delete for ORCID iD {user_id}."

                if user_option == 2:
                    if len(Block.query.filter_by(orcid=user_id).all()) > 0:
                        alerts["info"] = f"User is already banned: {user_id}"
                    else:
                        user = Block(orcid=user_id, name=get_orcid_name(api, user_id))
                        db.session.add(user)
                        db.session.commit()
                        alerts["success"] = f"User banned: {user_id}"

                if user_option == 3:
                    result = Block.query.filter_by(orcid=user_id).all()
                    if len(result) > 0:
                        Block.query.filter_by(orcid=user_id).delete()
                        db.session.commit()
                        alerts["success"] = f"Ban removed for ORCID iD: {user_id}"
                    else:
                        alerts["info"] = "ORCID iD is not banned."

        # Download database file
        if request.form.get("mode") == "backup_db":
            return send_file(config.dbpath, as_attachment=True)

        # Delete database orphans
        if request.form.get("mode") == "delete_orphans":
            Campaign.query.filter_by(action_slug='').delete()
            db.session.commit()
            alerts["success"] = "Deleted orphan campaigns"
            orphans = 0

    # Create a list of administrators and editors and count all users
    admins = Admin.query.filter_by(role_id=3).order_by(Admin.name.asc()).all()
    editors = Admin.query.filter_by(role_id=2).order_by(Admin.name.asc()).all()
    blocked = Block.query.order_by(Block.name.asc()).all()

    data = {
        "header_title": session["name"],
        "header_subtitle": session["orcid"],
        "header_path": editor_URI,
        "name": session["name"],
        "orcid_id": session["orcid"],
        "role": role.name.capitalize(),
        "role_id": role.role_id,
        "modify_options": modify_options,
        "delete_options": delete_options,
        "alert": alerts,
        "editors": editors,
        "admins": admins,
        "blocked": blocked,
        "orphans": orphans,
        "page": 'admin'
    }

    # Serve the admin page
    return render_template("admin.html", **(base_data | data))


@app.route(create_URI, methods=["POST", "GET"])
def create():
    # Show the page to create a campaign
    if session.get("orcid") is None:
        return redirect(home_URI)
    elif base_data["everyone_is_editor"] is False:
        # Query database for user's ORCID
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            print("User is not in the Admin database")
            return redirect(insufficient_privileges_URI)

        # Check if the user has sufficient permissions
        role_id = user.role_id
        if role_id < 2:
            print("Insufficient permissions to view this page")
            return redirect(insufficient_privileges_URI)
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2

    # Default alerts (= None)
    alerts = base_alerts.copy()

    new_campaign = Campaign(
        action_slug="",
        action_kind="",
        action_name="",
        action_short_description="",
        action_text="",
        sort_alphabetical=False,
        allow_anonymous=True,
        owner_orcid=None,
        is_active=True,
    )
    # If an update is pushed
    if request.method == "POST":
        if request.form.get("mode") == "create_campaign":
            if request.form["sort_alphabetical"] == "True":
                sort_alphabetical = True
            else:
                sort_alphabetical = False
            if request.form["allow_anonymous"] == "True":
                allow_anonymous = True
            else:
                allow_anonymous = False
            if request.form["activate_campaign"] == 'True':
                is_active = True
                closed_date = None
            else:
                is_active = False
                closed_date = datetime.datetime.now(datetime.UTC)

            action_slug = escape(request.form["action_slug"])
            new_campaign = Campaign(
                action_slug=action_slug,
                action_kind=escape(request.form["action_kind"]),
                action_name=escape(request.form["action_name"]),
                action_short_description=escape(request.form["action_short_description"]),
                action_text=request.form["action_text"],
                sort_alphabetical=sort_alphabetical,
                allow_anonymous=allow_anonymous,
                owner_orcid=session["orcid"],
                owner_name=session["name"],
                is_active=is_active,
                closed_date=closed_date,
            )

            if Campaign.query.filter_by(action_slug=action_slug).first() is not None:
                alerts["danger"] = "Action slug already exists. Please choose another."
            elif action_slug == '':
                alerts["danger"] = "Action slug cannot be an empty string."
            elif ' ' in action_slug:
                alerts["danger"] = "Action slug cannot contain spaces."
            elif action_slug in reserved_actions:
                alerts["danger"] = "Action slug is reserved. Please choose another."
            else:
                db.session.add(new_campaign)
                db.session.commit()

                base_data["redirect_alerts"] = {
                    "success": "Campaign created.",
                    "danger": None,
                    "info": None,
                    "warning": None,
                }
                return redirect(editor_URI)

    data = {
        "header_title": session["name"],
        "header_subtitle": session["orcid"],
        "header_path": editor_URI,
        "name": session["name"],
        "orcid_id": session["orcid"],
        "role_id": role_id,
        "alert": alerts,
        "page": 'create',
        "form_slug": new_campaign.action_slug,
        "form_kind": new_campaign.action_kind,
        "form_name": new_campaign.action_name,
        "form_short_description": new_campaign.action_short_description,
        "form_text": new_campaign.action_text,
        "form_sort_alphabetical": new_campaign.sort_alphabetical,
        "form_allow_anonymous": new_campaign.allow_anonymous,
        "form_activate_campaign": new_campaign.is_active,
    }

    return render_template("create.html", **(base_data | data))


@app.route(editor_URI)
def editor():
    # Show the editor page with their list of campaigns
    if session.get("orcid") is None:
        return redirect(home_URI)
    elif base_data["everyone_is_editor"] is False:
        # Query database for user's ORCID
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            print("User is not in the Admin database")
            return redirect(insufficient_privileges_URI)

        # Check if the user has sufficient permissions
        role_id = user.role_id
        if role_id < 2:
            print("Insufficient permissions to view this page")
            return redirect(insufficient_privileges_URI)
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2

    # Default alerts (= None)
    if base_data["redirect_alerts"] is None:
        alerts = base_alerts.copy()
    else:
        alerts = base_data["redirect_alerts"]
        base_data["redirect_alerts"] = None

    my_campaigns = dict()
    all_campaigns = dict()
    # Create list of signatory campaigns
    for row in Campaign.query.order_by(Campaign.action_name.asc()).all():
        if row.owner_orcid == session["orcid"]:
            my_campaigns[row.action_slug] = [
                row.action_name, row.action_short_description,
                os.path.join(config.site_path, row.action_slug),
                row.is_active
            ]

    if role_id == 3:
        for row in Campaign.query.order_by(Campaign.action_name.asc()).all():
            all_campaigns[row.action_slug] = [
                row.action_name, row.action_short_description,
                os.path.join(config.site_path, row.action_slug),
                row.is_active
            ]

    data = {
        "header_title": session["name"],
        "header_subtitle": session["orcid"],
        "header_path": editor_URI,
        "name": session["name"],
        "orcid_id": session["orcid"],
        "role_id": role_id,
        "alert": alerts,
        "page": 'editor',
        "my_campaigns": my_campaigns,
        "all_campaigns": all_campaigns,
    }

    return render_template("editor.html", **(base_data | data))


@app.route(edit_URI, methods=["POST", "GET"])
def edit(slug):
    # Show the page to edit a specific campaign
    if session.get("orcid") is None:
        return redirect(home_URI)
    elif base_data["everyone_is_editor"] is False:
        # Query database for user's ORCID
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            print("User is not in the Admin database")
            return redirect(insufficient_privileges_URI)

        # Check if the user has sufficient permissions
        role_id = user.role_id
        if role_id < 2:
            print("Insufficient permissions to view this page")
            return redirect(insufficient_privileges_URI)
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2

    edit_campaign = Campaign.query.filter_by(action_slug=slug).first()
    if not edit_campaign:
        return render_template("campaign-not-found.html", **(base_data))

    # For editors, check if the user is the campaign owner
    if role_id == 2:
        if edit_campaign.owner_orcid != session["orcid"]:
            print("Insufficient permissions to edit this action")
            return redirect(insufficient_privileges_URI)

    # Default alerts (= None)
    alerts = base_alerts.copy()

    # If an update is pushed
    if request.method == "POST":
        if request.form.get("mode") == "edit_campaign":
            if request.form["sort_alphabetical"] == "True":
                sort_alphabetical = True
            else:
                sort_alphabetical = False
            if request.form["allow_anonymous"] == "True":
                allow_anonymous = True
            else:
                allow_anonymous = False

            edit_campaign.action_kind = escape(request.form["action_kind"])
            edit_campaign.action_name = escape(request.form["action_name"])
            edit_campaign.action_short_description = escape(request.form["action_short_description"])
            edit_campaign.action_text = request.form["action_text"]
            edit_campaign.sort_alphabetical = sort_alphabetical
            edit_campaign.allow_anonymous = allow_anonymous

            db.session.commit()

            base_data["redirect_alerts"] = {
                "success": "Campaign updated.",
                "danger": None,
                "info": None,
                "warning": None,
            }
            return redirect(editor_URI)

        if request.form.get("mode") == "close_activate":
            if request.form["is_active"] == "Active":
                is_active = True
                edit_campaign.closed_date = None
                alert_text = "Campaign activated."
            else:
                is_active = False
                edit_campaign.closed_date = datetime.datetime.now(datetime.UTC)
                alert_text = "Campaign deactivated."

            edit_campaign.is_active = is_active
            db.session.commit()

            base_data["redirect_alerts"] = {
                "success": alert_text,
                "danger": None,
                "info": None,
                "warning": None,
            }
            return redirect(editor_URI)

        if request.form.get("mode") == "reset_date":
            edit_campaign.creation_date = datetime.datetime.now(datetime.UTC)
            db.session.commit()

            base_data["redirect_alerts"] = {
                "success": "Campaign creation date updated.",
                "danger": None,
                "info": None,
                "warning": None,
            }
            return redirect(editor_URI)

        if request.form.get("mode") == "delete_campaign":
            if request.form["confirmation"].lower() == "delete":
                # delete campaign and all signatories
                Campaign.query.filter_by(action_slug=slug).delete()
                Signatory.query.filter_by(campaign=slug).delete()
                db.session.commit()
                base_data["redirect_alerts"] = {
                    "success": "Campaign deleted.",
                    "danger": None,
                    "info": None,
                    "warning": None,
                }
                return redirect(editor_URI)
            else:
                alerts["danger"] = "Please confirm your response with \"delete\"."
            db.session.commit()

    data = {
        "header_title": session["name"],
        "header_subtitle": session["orcid"],
        "header_path": editor_URI,
        "name": session["name"],
        "orcid_id": session["orcid"],
        "role_id": role_id,
        "owner_orcid": edit_campaign.owner_orcid,
        "owner_name": edit_campaign.owner_name,
        "creation_date": edit_campaign.creation_date,
        "alert": alerts,
        "page": 'editor',
        "form_kind": edit_campaign.action_kind,
        "form_name": edit_campaign.action_name,
        "form_short_description": edit_campaign.action_short_description,
        "form_text": edit_campaign.action_text,
        "form_sort_alphabetical": edit_campaign.sort_alphabetical,
        "form_allow_anonymous": edit_campaign.allow_anonymous,
        "is_active": edit_campaign.is_active,
    }

    return render_template("edit.html", **(base_data | data))


@app.route(thank_you_URI)
def thank_you(slug):
    # Show page thanking the user for signing the campaign
    if session.get("orcid") is None:
        return redirect(home_URI)
    elif base_data["everyone_is_editor"] is True:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            role_id = 0
        else:
            role_id = user.role_id

    action_data = Campaign.query.filter_by(action_slug=slug).first()

    data = {
        "header_title": action_data.action_name,
        "header_subtitle": action_data.action_kind.upper(),
        "header_path": os.path.join(config.site_path, slug),
        "action_kind": action_data.action_kind,
        "role_id": role_id,
    }
    base_data["user_URI_defined"] = None
    base_data["thank_you_URI_defined"] = None
    base_data["signature_removed_URI_defined"] = None

    # If a user session exists, close it (if admin, do nothing)
    if role_id == 0:
        session.pop("name", None)
        session.pop("orcid", None)

    return render_template("thank-you.html", **(base_data | data))


@app.route(signature_removed_URI)
def signature_removed(slug):
    # Show page confirming that the user signature was removed
    if session.get("orcid") is None:
        return redirect(home_URI)
    elif base_data["everyone_is_editor"] is True:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is not None:
            role_id = user.role_id
        else:
            role_id = 2
    else:
        user = Admin.query.filter_by(orcid=session["orcid"]).first()
        if user is None:
            role_id = 0
        else:
            role_id = user.role_id

    action_data = Campaign.query.filter_by(action_slug=slug).first()

    data = {
        "header_title": action_data.action_name,
        "header_subtitle": action_data.action_kind.upper(),
        "header_path": os.path.join(config.site_path, slug),
        "action_kind": action_data.action_kind,
        "role_id": role_id,
    }
    base_data["user_URI_defined"] = None
    base_data["thank_you_URI_defined"] = None
    base_data["signature_removed_URI_defined"] = None

    # If a user session exists, close it (if admin, do nothing)
    if role_id == 0:
        session.pop("name", None)
        session.pop("orcid", None)

    return render_template("signature-removed.html", **(base_data | data))


@app.route(insufficient_privileges_URI)
def insufficient_privileges():
    data = {
        "header_title": config.site_title,
        "header_subtitle": config.site_subtitle,
        "header_path": config.site_path,
    }
    return render_template("insufficient-privileges.html", **(base_data | data))


@app.route(logout_URI)
def logout():
    # If a user session exists, close it
    if session.get("orcid") is not None:
        session.pop("name", None)
        session.pop("orcid", None)

    base_data["user_URI_defined"] = None
    base_data["thank_you_URI_defined"] = None
    base_data["signature_removed_URI_defined"] = None

    return redirect(home_URI)


@app.route(banned_URI)
def banned():
    # If a user session exists, close it
    if session.get("orcid") is not None:
        session.pop("name", None)
        session.pop("orcid", None)

        data = {
            "header_title": config.site_title,
            "header_subtitle": config.site_subtitle,
            "header_path": config.site_path,
            "role_id": 0,
        }
        return render_template("user-banned.html", **(base_data | data))
    else:
        return redirect(home_URI)


@app.errorhandler(404)
def page_not_found(e):
    data = {
        "header_title": config.site_title,
        "header_subtitle": config.site_subtitle,
        "header_path": config.site_path,
    }
    return render_template("404.html", **(base_data | data)), 404


@app.route('/feed/')
def feeds():
    fg = FeedGenerator()
    fg.id(os.path.join(config.site_path, "feeds"))
    fg.title(config.site_title)
    fg.subtitle(config.site_subtitle)
    fg.link(href=config.site_path, rel='alternate')
    fg.language('en')

    # Create list of feed entries
    for row in Campaign.query.filter_by(is_active=True).order_by(Campaign.creation_date.asc()).all():
        if row.action_slug not in ['demo', 'demo-no-anonymous']:
            fe = fg.add_entry()
            fe.id(os.path.join(config.site_path, row.action_slug))
            fe.title(row.action_name)
            fe.summary(row.action_short_description)
            fe.link(href=os.path.join(config.site_path, row.action_slug))
            fe.published(row.creation_date.replace(tzinfo=datetime.UTC))
            fe.content(row.action_text)

    # Generate the feed as bytes
    feed_data = fg.atom_str(pretty=True)

    # Create a BytesIO object
    feed_io = BytesIO(feed_data)

    # Return as downloadable file
    return send_file(
        feed_io,
        as_attachment=False,
        download_name='atom.xml',
        mimetype='application/atom+xml'
    )


if __name__ == "__main__":
    if config.sandbox:
        app.run(host="127.0.0.1", port=config.port, debug=True)
    else:
        serve(app, host="127.0.0.1", port=config.port)

back to top

Software Heritage — Copyright (C) 2015–2026, The Software Heritage developers. License: GNU AGPLv3+.
The source code of Software Heritage itself is available on our development forge.
The source code files archived by Software Heritage are available under their own copyright and licenses.
Terms of use: Archive access, API— Content policy— Contact— JavaScript license information— Web API