├── core ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_category_slug.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── makeform.py ├── utils.py ├── models.py ├── context_processors.py ├── middleware.py └── fixtures │ └── default_choices.json ├── mos ├── __init__.py ├── settings │ ├── __init__.py │ ├── devel.py │ ├── deploy.py.tpl │ ├── deploy_env.py │ └── common.py ├── asgi.py └── urls.py ├── things ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_remove_thing_name.py │ ├── 0007_alter_thingevent_usage_seconds.py │ ├── 0009_thing_expiry_notice.py │ ├── 0008_alter_thingevent_kind.py │ ├── 0002_auto_20240224_1354.py │ ├── 0005_auto_20240224_1447.py │ ├── 0003_auto_20240224_1415.py │ ├── 0001_initial.py │ └── 0006_thingevent.py ├── apps.py ├── urls.py ├── management │ └── commands │ │ └── send_thing_expiration_notices.py ├── admin.py ├── models.py └── views.py ├── web ├── __init__.py ├── tests.py └── views.py ├── announce ├── __init__.py ├── urls.py ├── templates │ └── announce │ │ ├── message_sent.html │ │ └── write_message.html └── views.py ├── members ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_contact_info.py │ └── test_membership_period.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── domail.py │ │ ├── list_intern_emails.py │ │ ├── member_categories.py │ │ ├── generate_many_members.py │ │ └── invite_matrix_users.py ├── migrations │ ├── __init__.py │ ├── 0017_merge_20240224_1514.py │ ├── 0024_alter_kindofmembership_options.py │ ├── 0006_alter_contactinfo_last_email_ok.py │ ├── 0009_pendingpayment_original_file.py │ ├── 0016_membershipperiod_comment.py │ ├── 0013_alter_bankimportmatcher_comment.py │ ├── 0023_contactinfo_gdpr_wiped_on.py │ ├── 0016_auto_20240224_1348.py │ ├── 0018_remove_paymentinfo_bank_account_number_and_more.py │ ├── 0002_auto_20221112_1256.py │ ├── 0007_alter_contactinfo_image.py │ ├── 0010_alter_paymentinfo_bank_account_iban.py │ ├── 0005_mailinglistmail.py │ ├── 0021_communicationrecord_monthly_fee_at_contact_and_more.py │ ├── 0022_contactinfo_in_intern_matrix_room_and_more.py │ ├── 0025_alter_contactinfo_matrix_handle.py │ ├── 0019_pendingpayment_creator.py │ ├── 0012_bankimportmatcher.py │ ├── 0003_auto_20221112_1308.py │ ├── 0014_auto_20230709_2019.py │ ├── 0004_auto_20221112_1422.py │ ├── 0011_locker.py │ ├── 0015_auto_20230709_2031.py │ ├── 0008_pendingpayment.py │ └── 0020_communicationrecord.py ├── templates │ └── admin │ │ ├── change_form.html │ │ └── wipe_members.html ├── middleware.py ├── forms.py ├── urls.py └── util.py ├── projects ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── admin.py ├── templates │ └── projects │ │ ├── project_archive.html │ │ ├── project_detail.html │ │ ├── overview.inc │ │ └── projectinfo.inc ├── forms.py ├── models.py ├── urls.py └── views.py ├── sources ├── __init__.py ├── tests │ ├── __init__.py │ └── test_cronjob.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── get_wiki_changes.py │ │ └── jour_fixe_reminder.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py └── models.py ├── cal ├── migrations │ ├── __init__.py │ ├── 0002_event_advertise.py │ ├── 0003_alter_event_advertise.py │ ├── 0004_event_wikiimagepage.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── cal_tags.py ├── admin.py ├── __init__.py ├── fixtures │ └── initial_user.json ├── feeds.py ├── forms.py └── urls.py ├── static ├── stylesheets │ ├── monobook.css │ ├── metanight.png │ ├── base.css │ ├── projects.css │ ├── members.css │ ├── reset.css │ ├── soup_startpage.css │ ├── cellardoor.css │ ├── global.css │ └── layout.css ├── images │ ├── bg.gif │ ├── fail.jpg │ ├── logo.gif │ ├── logo.png │ ├── pixel.gif │ ├── Flag_UK.png │ ├── atomic.gif │ ├── pixel_blue.png │ ├── soup_logo.png │ ├── logo_trauer.png │ ├── metasense_on.gif │ ├── default_avatar.png │ └── metasense_off.gif └── javascripts │ ├── sorttable.js │ ├── formset_handler.js │ └── ml-ajaxtools.js ├── templates ├── 404.html ├── members │ ├── contact_info.inc │ ├── new_member_welcome.mail.subject │ ├── members_details_error.inc │ ├── member_update_userpic.html │ ├── member_bank.html │ ├── members_details.html │ ├── member_list.html │ ├── members_list.html │ ├── member_list.inc │ ├── members_history.html │ ├── member_list_superuser.inc │ ├── members_hetti.html │ └── member_bank_json_match.html ├── things │ ├── thing_expiration_notice.mail.subject │ └── thing_expiration_notice.mail ├── rss │ ├── change_archive.html │ └── recentchanges.inc ├── 500.html ├── jour_fixe_reminder.mail.subject ├── registration │ ├── logged_out.html │ ├── password_change_done.html │ ├── login.html │ └── password_change_form.html ├── auth │ ├── user_list_mainpage.inc │ ├── user_list.html │ ├── user_list.inc │ └── user_list_superuser.inc ├── cal │ ├── event_detail.html │ ├── calendar.inc │ ├── event_archive.html │ ├── event_archive_year.html │ ├── eventinfo_nf.inc │ ├── eventinfo_detail.inc │ └── event_form.inc ├── welcome.mail ├── jour_fixe_reminder.mail ├── cellardoor.html ├── index.html └── base.html ├── .dockerignore ├── requirements-dev.txt ├── .gitignore ├── requirements.txt ├── HACKING_WITH_VAGRANT ├── HACKING_WITH_DOCKER ├── bootstrap_ansible.sh ├── .pre-commit-config.yaml ├── manage.py ├── docker ├── Dockerfile ├── entrypoint.sh └── docker-compose.yml ├── README.rst ├── docs └── THINGS.md ├── wsgi.py ├── HACKING ├── Vagrantfile └── provision_vagrant.yml /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /things/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /announce/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /members/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cal/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cal/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /members/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mos/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /members/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /members/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/stylesheets/monobook.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | ohai 404! 2 | -------------------------------------------------------------------------------- /things/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /docker/volume-* 2 | -------------------------------------------------------------------------------- /templates/members/contact_info.inc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /members/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sources/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/members/new_member_welcome.mail.subject: -------------------------------------------------------------------------------- 1 | Willkommen im Metalab 2 | -------------------------------------------------------------------------------- /static/images/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/bg.gif -------------------------------------------------------------------------------- /static/images/fail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/fail.jpg -------------------------------------------------------------------------------- /static/images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/logo.gif -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/logo.png -------------------------------------------------------------------------------- /static/images/pixel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/pixel.gif -------------------------------------------------------------------------------- /static/images/Flag_UK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/Flag_UK.png -------------------------------------------------------------------------------- /static/images/atomic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/atomic.gif -------------------------------------------------------------------------------- /templates/members/members_details_error.inc: -------------------------------------------------------------------------------- 1 | you are not allowed to see the profile of the user 2 | -------------------------------------------------------------------------------- /static/images/pixel_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/pixel_blue.png -------------------------------------------------------------------------------- /static/images/soup_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/soup_logo.png -------------------------------------------------------------------------------- /static/images/logo_trauer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/logo_trauer.png -------------------------------------------------------------------------------- /static/images/metasense_on.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/metasense_on.gif -------------------------------------------------------------------------------- /static/images/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/default_avatar.png -------------------------------------------------------------------------------- /static/images/metasense_off.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/images/metasense_off.gif -------------------------------------------------------------------------------- /static/javascripts/sorttable.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/javascripts/sorttable.js -------------------------------------------------------------------------------- /static/stylesheets/metanight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metalab/mos/HEAD/static/stylesheets/metanight.png -------------------------------------------------------------------------------- /templates/things/thing_expiration_notice.mail.subject: -------------------------------------------------------------------------------- 1 | ACHTUNG: Dein metalab Zugang: {{thing}} läuft bald ab! 2 | -------------------------------------------------------------------------------- /projects/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Project 4 | 5 | admin.site.register(Project) 6 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ipython 2 | flake8 3 | autopep8 4 | django_debug_toolbar 5 | autoflake 6 | black 7 | isort 8 | pre-commit 9 | -------------------------------------------------------------------------------- /announce/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | import announce.views 4 | 5 | urlpatterns = [ 6 | path('', announce.views.announce), 7 | ] 8 | -------------------------------------------------------------------------------- /templates/rss/change_archive.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 | {% include "rss/recentchanges.inc" %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /projects/templates/projects/project_archive.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 | {% include "projects/overview.inc" %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 |

500

2 | 3 |

Whoops, something happened. We have been notified and will look into it - promise!

4 | -------------------------------------------------------------------------------- /templates/jour_fixe_reminder.mail.subject: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% language 'de' %} 3 | [REMINDER] Jour Fixe am {{ jf.startDate | date:"l, Y-m-d" }} 4 | {% endlanguage %} 5 | -------------------------------------------------------------------------------- /things/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ThingsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'things' 7 | -------------------------------------------------------------------------------- /static/stylesheets/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | DJANGO Admin 3 | by Wilson Miner wilson@lawrence.com 4 | */ 5 | 6 | /* Import other styles */ 7 | @import url('reset.css'); 8 | @import url('global.css'); 9 | @import url('layout.css'); 10 | -------------------------------------------------------------------------------- /templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 | 5 |

You have been logged out.

6 | 7 |

'Log in again'

8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.sqlite 3 | 4 | mos/settings/local* 5 | mos/settings/secret_key.* 6 | web/performance.log 7 | mos_env/ 8 | env/ 9 | 10 | logs/ 11 | media/ 12 | 13 | .vagrant 14 | /docker/volume-* 15 | 16 | matrix_client_store/ 17 | -------------------------------------------------------------------------------- /templates/auth/user_list_mainpage.inc: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /cal/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Category 4 | from .models import Event 5 | from .models import Location 6 | 7 | admin.site.register(Event) 8 | admin.site.register(Category) 9 | admin.site.register(Location) 10 | -------------------------------------------------------------------------------- /templates/auth/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block hos_content %} 3 | {% if user.is_superuser %} 4 | {% include 'auth/user_list_superuser.inc' %} 5 | {% else %} 6 | {% include "auth/user_list.inc" %} 7 | {% endif %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /members/templates/admin/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load static %} 3 | 4 | {% block admin_change_form_document_ready %} 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /static/stylesheets/projects.css: -------------------------------------------------------------------------------- 1 | .project .hoverHidden { 2 | visibility: hidden; 3 | } 4 | 5 | .project:hover .hoverHidden { 6 | visibility: visible; 7 | opacity: 0.6; 8 | } 9 | 10 | .project:hover .hoverHidden:hover { 11 | opacity: 1.0; 12 | } 13 | -------------------------------------------------------------------------------- /templates/rss/recentchanges.inc: -------------------------------------------------------------------------------- 1 |

Letzte Änderungen

2 | {% for change in latestchanges %} 3 | {{ change.title }}
- {{ change.author }}
4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /things/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('keys/', views.thingusers_list), 7 | path('usage/', views.thingusers_usage), 8 | path('stats/', views.thing_usage_stats), 9 | ] 10 | -------------------------------------------------------------------------------- /templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Metalab - Password change successful{% endblock %} 4 | 5 | {% block hos_content %} 6 | 7 |

Password change successful

8 | 9 |

Your password was changed.

10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/cal/event_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load makeform %} 3 | 4 | {% block title %}Metalab Event: {{ event.name }}{% endblock %} 5 | {% block hos_content %} 6 | {% makeform event cal.forms.EventForm event_form %} 7 | {% include "cal/eventinfo_detail.inc" with new=0 %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /members/middleware.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import logout 2 | from django.utils.deprecation import MiddlewareMixin 3 | 4 | 5 | class DeactivateUserMiddleware(MiddlewareMixin): 6 | def process_request(self, request): 7 | if request.user.is_authenticated and not request.user.is_active: 8 | return logout(request) 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django~=4.2.10 2 | Pillow 3 | icalendar~=5.0.11 4 | Unipath 5 | django-extensions 6 | python-dateutil 7 | freezegun 8 | feedparser 9 | mysqlclient 10 | channels 11 | websockets 12 | sepaxml 13 | daphne 14 | requests 15 | easy-thumbnails 16 | beautifulsoup4==4.12.3 17 | matrix-commander==7.6.1 18 | django-import-export==4.3.14 19 | -------------------------------------------------------------------------------- /projects/templates/projects/project_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load makeform %} 3 | 4 | {% block title %}Metalab Project: {{ project.name }}{% endblock %} 5 | {% block hos_content %} 6 | {% makeform project projects.forms.ProjectForm project_form %} 7 |
  • {% include "projects/projectinfo_nf.inc" with new=0 %}
  • 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /sources/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class WikiChange(models.Model): 5 | title = models.CharField(max_length=200) 6 | link = models.CharField(max_length=300) 7 | author = models.CharField(max_length=300) 8 | updated = models.DateTimeField() 9 | 10 | def __str__(self): 11 | return '%s: %s' % (self.author, self. title) 12 | -------------------------------------------------------------------------------- /HACKING_WITH_VAGRANT: -------------------------------------------------------------------------------- 1 | enter the VM with: 2 | $ vagrant ssh 3 | 4 | the following commands have to be executed inside the VM! 5 | 6 | 1) after provisioning the VM you have to set the 'admin' superuser password manually 7 | $ ./manage.py changepassword admin 8 | 9 | 2) start the server bound on all interfaces instead of 127.0.0.1 only 10 | $ ./manage.py runserver 0.0.0.0:8000 11 | -------------------------------------------------------------------------------- /HACKING_WITH_DOCKER: -------------------------------------------------------------------------------- 1 | Install docker and docker-compose on your host OS (example for Debian/Ubuntu): 2 | 3 | sudo apt install docker.io docker-compose 4 | 5 | Then, build and run the Docker image: 6 | 7 | docker-compose -f docker/docker-compose.yml run --service-ports mos 8 | 9 | After it is up and running, point your web browser to: 10 | 11 | http://localhost:8020/ 12 | -------------------------------------------------------------------------------- /members/migrations/0017_merge_20240224_1514.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 15:14 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('members', '0016_auto_20240224_1348'), 10 | ('members', '0016_membershipperiod_comment'), 11 | ] 12 | 13 | operations = [ 14 | ] 15 | -------------------------------------------------------------------------------- /bootstrap_ansible.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | 4 | sudo apt-get update 5 | # install requirements for ansible and python 6 | sudo apt-get install -y curl apt-transport-https python-setuptools libmariadb-dev 7 | sudo apt-get install ansible -y --no-install-recommends 8 | # run ansible provisioning tasks 9 | PYTHONUNBUFFERED=1 ansible-playbook -i "localhost," -c local /vagrant/provision_vagrant.yml 10 | -------------------------------------------------------------------------------- /members/management/commands/domail.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from members.models import get_active_members 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | def handle(self, *args, **kwargs): 9 | for user in get_active_members(): 10 | user.is_active = True 11 | user.save() 12 | 13 | print(user, user.email) 14 | -------------------------------------------------------------------------------- /members/tests/test_contact_info.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | 4 | from members.models import ContactInfo 5 | 6 | 7 | class ContactInfoTest(TestCase): 8 | def test_get_date_of_entry_without_membership_period(self): 9 | user = User.objects.create() 10 | info = ContactInfo(user=user) 11 | info.get_date_of_first_join() # does not raise 12 | -------------------------------------------------------------------------------- /templates/members/member_update_userpic.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block hos_content %} 4 |

    Change your userpic

    5 | 6 | {% csrf_token %} 7 | {{form.as_table}} 8 | 9 | 10 | 11 |
    12 | {% endblock hos_content %} 13 | -------------------------------------------------------------------------------- /projects/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import TextInput 2 | from django.forms.models import ModelForm 3 | 4 | from .models import Project 5 | 6 | 7 | class ProjectForm(ModelForm): 8 | """ 9 | From to add a Project 10 | """ 11 | 12 | class Meta: 13 | model = Project 14 | fields = ('name', 'teaser', 'wikiPage', 'finished_at') 15 | widgets = { 16 | 'teaser': TextInput, 17 | } 18 | -------------------------------------------------------------------------------- /things/migrations/0004_remove_thing_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 14:38 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('things', '0003_auto_20240224_1415'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='thing', 15 | name='name', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /cal/__init__.py: -------------------------------------------------------------------------------- 1 | from icalendar import Calendar 2 | 3 | 4 | def create_calendar(components): 5 | c = Calendar() 6 | c.add('version', '2.0') 7 | c.add('prodid', '-//Hackerspace OS//code.google.com/p/hackerspace-os//') 8 | c.add('X-WR-TIMEZONE', 'Europe/Vienna') 9 | c.add('X-WR-CALNAME', 'Metalab') 10 | c.add('X-WR-CALDESC', 'Metalab Events Calendar') 11 | for component in components: 12 | c.add_component(component) 13 | return c 14 | -------------------------------------------------------------------------------- /templates/members/member_bank.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 |

    5 |

    Erste Bank JSON import

    6 |
    7 | {% csrf_token %} 8 |
    9 | 10 |
    11 |

    12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /core/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | used from pylucid (www.pylucid.org) 3 | see http://trac.pylucid.net/browser/trunk/pylucid/PyLucid/template_addons/\ 4 | filters.py for author 5 | information 6 | """ 7 | 8 | 9 | def human_readable_time(t): 10 | """ converts (milli-)seconds into a nice string """ 11 | 12 | if t < 1: 13 | return ("%.1f ms") % (t * 1000) 14 | elif t > 60: 15 | return ("%.1f min") % (t / 60.0) 16 | else: 17 | return ("%.1f sec") % t 18 | -------------------------------------------------------------------------------- /members/migrations/0024_alter_kindofmembership_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-11-23 13:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("members", "0023_contactinfo_gdpr_wiped_on"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="kindofmembership", 15 | options={"ordering": ["name"]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /core/migrations/0002_category_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-11-22 17:07 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("core", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="category", 16 | name="slug", 17 | field=models.SlugField(null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cal/migrations/0002_event_advertise.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-11-25 21:08 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('cal', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='event', 16 | name='advertise', 17 | field=models.BooleanField(default=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cal/migrations/0003_alter_event_advertise.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 15:30 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('cal', '0002_event_advertise'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='event', 16 | name='advertise', 17 | field=models.BooleanField(default=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /templates/welcome.mail: -------------------------------------------------------------------------------- 1 | Hello {{ user.first_name }}, 2 | 3 | Wir haben dir einen User auf der neuen Metalab-Webseite angelegt. 4 | 5 | Zugangsdaten: http://metalab.at/login/ l/p: {{ user.username }}/{{newpass}} 6 | 7 | Dort kannst du Events und Projekte bearbeiten, und dein Member-Profil einsehen. 8 | 9 | Weitere Features folgen in Kürze! 10 | 11 | Dein MOS-Team 12 | 13 | P.S. Bei Problemen und für Anregungen nutze bitte das Projekt auf Github unter https://github.com/Metalab/mos 14 | und lege entsprechende Issues an :-) 15 | -------------------------------------------------------------------------------- /cal/migrations/0004_event_wikiimagepage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-11-23 13:25 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("cal", "0003_alter_event_advertise"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="event", 16 | name="wikiImagePage", 17 | field=models.CharField(blank=True, max_length=200), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /members/migrations/0006_alter_contactinfo_last_email_ok.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-11-25 23:55 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0005_mailinglistmail'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='contactinfo', 16 | name='last_email_ok', 17 | field=models.BooleanField(null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /members/migrations/0009_pendingpayment_original_file.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-08 15:21 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0008_pendingpayment'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='pendingpayment', 16 | name='original_file', 17 | field=models.CharField(max_length=200, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /things/migrations/0007_alter_thingevent_usage_seconds.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 17:34 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('things', '0006_thingevent'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='thingevent', 16 | name='usage_seconds', 17 | field=models.PositiveIntegerField(blank=True, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /members/migrations/0016_membershipperiod_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 15:10 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0015_auto_20230709_2031'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='membershipperiod', 16 | name='comment', 17 | field=models.CharField(blank=True, max_length=200, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /members/migrations/0013_alter_bankimportmatcher_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-09 20:16 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0012_bankimportmatcher'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='bankimportmatcher', 16 | name='comment', 17 | field=models.CharField(blank=True, max_length=200, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /members/migrations/0023_contactinfo_gdpr_wiped_on.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-11-22 18:43 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("members", "0022_contactinfo_in_intern_matrix_room_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="contactinfo", 16 | name="gdpr_wiped_on", 17 | field=models.DateField(blank=True, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/PyCQA/autoflake 9 | rev: v2.2.1 10 | hooks: 11 | - id: autoflake 12 | args: [--remove-all-unused-imports, --in-place] 13 | - repo: https://github.com/pycqa/isort 14 | rev: 5.13.2 15 | hooks: 16 | - id: isort 17 | args: 18 | - --force-single-line-imports 19 | - --profile black 20 | -------------------------------------------------------------------------------- /cal/fixtures/initial_user.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 2, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "d3f3nd3r", 7 | "first_name": "Def", 8 | "last_name": "Ender", 9 | "is_active": true, 10 | "is_superuser": false, 11 | "is_staff": false, 12 | "last_login": "2013-07-12T23:21:54.671", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "pbkdf2_sha256$10000$3w7ogbZTpBXQ$PhnPg4jcsczCs8hGZLbiDi+KGFKhvz8mEXLnJ/L20aM=", 16 | "email": "defender@example.com", 17 | "date_joined": "2013-07-12T21:26:59" 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /members/migrations/0016_auto_20240224_1348.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 13:48 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('members', '0015_auto_20230709_2031'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='contactinfo', 15 | name='has_lazzzor_privileges', 16 | ), 17 | migrations.RemoveField( 18 | model_name='contactinfo', 19 | name='lazzzor_rate', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /announce/templates/announce/message_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 |

    Metalab Announcer

    5 | 6 |

    7 | Die Nachricht wurde versendet. 8 |

    9 | 10 |

    11 | Betreff: {{ form.cleaned_data.subject }} 12 |

    13 | 14 | Body: 15 |

    16 | {{ form.cleaned_data.body }} 17 |

    18 | 19 | Users ({{ users.count }}): 20 |
      21 | {% for u in users %} 22 |
    1. {{ u.username }} - {{ u.email|default:"(keine Mail-Adresse)" }}
    2. 23 | {% endfor %} 24 |
    25 | 26 |
    27 | Neue Nachricht. 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /members/migrations/0018_remove_paymentinfo_bank_account_number_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-02-01 15:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("members", "0017_merge_20240224_1514"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="paymentinfo", 15 | name="bank_account_number", 16 | ), 17 | migrations.RemoveField( 18 | model_name="paymentinfo", 19 | name="bank_code", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /cal/feeds.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.contrib.syndication.views import Feed 4 | from django.db.models import Q 5 | 6 | from .models import Event 7 | 8 | 9 | class EventFeed(Feed): 10 | title = 'Zukuenftige Veranstaltungen' 11 | link = '/' 12 | description = 'zukuenftige und laufende Veranstaltungen ' \ 13 | 'in und um den Wiener Hackerspace Metalab' 14 | 15 | def items(self): 16 | now = datetime.datetime.now() 17 | future = Q(startDate__gte=now) 18 | running = Q(startDate__lte=now) & Q(endDate__gte=now) 19 | return Event.all.filter(future|running).order_by('startDate') 20 | -------------------------------------------------------------------------------- /members/migrations/0002_auto_20221112_1256.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-11-12 12:56 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='kindofmembership', 16 | name='spind', 17 | field=models.CharField(blank=True, choices=[('small_1', '1 kleiner Spind'), ('small_2', '2 kleiner Spind'), ('big_1', '1 großer Spind')], max_length=7, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /members/migrations/0007_alter_contactinfo_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-08 15:04 2 | 3 | import easy_thumbnails.fields 4 | from django.db import migrations 5 | 6 | import members.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('members', '0006_alter_contactinfo_last_email_ok'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='contactinfo', 18 | name='image', 19 | field=easy_thumbnails.fields.ThumbnailerImageField(blank=True, upload_to=members.models.get_image_path), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /web/tests.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from django.test import TestCase 4 | from django.test.client import Client 5 | 6 | 7 | class PerformanceMainPageTest(TestCase): 8 | 9 | def setUp(self): 10 | self.client = Client() 11 | 12 | def testLoadTime(self): 13 | with open('web/performance.log', 'a+') as log_file: 14 | for i in range(1, 40): 15 | start = time.time() 16 | self.client.get('/') 17 | end = time.time() 18 | load_time = end - start 19 | 20 | log_str = 'MainPage : %f \n ' % (load_time) 21 | log_file.write(log_str) 22 | -------------------------------------------------------------------------------- /members/migrations/0010_alter_paymentinfo_bank_account_iban.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-08 21:06 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | import members.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('members', '0009_pendingpayment_original_file'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='paymentinfo', 18 | name='bank_account_iban', 19 | field=models.CharField(blank=True, max_length=34, validators=[members.models.iban_validate]), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /things/migrations/0009_thing_expiry_notice.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-11-22 16:16 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("things", "0008_alter_thingevent_kind"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="thing", 16 | name="expiry_notice", 17 | field=models.TextField( 18 | default="", 19 | help_text="Expiry notice text sent with the expiry notice email", 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | # Dirty hack. Right now, we need this in order for the settings module 7 | # "mos.settings.devel" to be found (plus many other things that import 8 | # "mos and its child modules/packages). Ideally we should create a new 9 | # package "mos" in this directory and move all files there, and then 10 | # remove this line. 11 | #sys.path.insert(0, '..') 12 | 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mos.settings.devel") 14 | 15 | from django.core.management import execute_from_command_line 16 | 17 | execute_from_command_line(sys.argv) 18 | -------------------------------------------------------------------------------- /members/templates/admin/wipe_members.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load l10n %} 3 | {% block content %} 4 |

    DSGVO-Schredererer

    5 |
    {% csrf_token %} 6 | Sollen die folgenden {{ queryset|length }} user wirklich ge-DSGVOwiped-werden? 7 |
      8 | {% for obj in queryset %} 9 | 10 |
    • {{ obj }}
    • 11 | {% endfor %} 12 |
    13 | 14 | 15 | 16 |
    17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/members/members_details.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block add_css %} 4 | 5 | {% endblock %} 6 | 7 | {{ error_form|default_if_none:"" }} 8 | {{ error_type|default_if_none:"" }} 9 | 10 | {% block hos_content %} 11 |

    User profile: {{ item }}

    12 | {% if user.is_superuser %} 13 | {% include 'members/members_details.inc' %} 14 | {% else %} 15 | {% if user.id == item.id %} 16 | {% include 'members/members_details.inc' %} 17 | {% else %} 18 | {% include 'members/members_details_error.inc' %} 19 | {% endif %} 20 | {% endif %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /projects/templates/projects/overview.inc: -------------------------------------------------------------------------------- 1 |
    2 | {% if errors %} 3 |
    4 | Following error(s) occured while editing {{ e_project_name }} :
    5 | {{errors}} 6 | Ok 7 |
    8 | {% endif %} 9 |
      10 | {% for project in latestprojects %} 11 |
    • {% include "projects/projectinfo.inc" %}
    • 12 | {% endfor %} 13 |
    • {% include "projects/projectinfo.inc" %}
    • 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /templates/members/member_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% if user.is_superuser %} 5 | 6 | {% block add_css %} 7 | 12 | {% endblock %} 13 | 14 | {% block add_js %} 15 | 16 | {% endblock %} 17 | {% endif %} 18 | 19 | {% block hos_content %} 20 | {% if user.is_superuser %} 21 | {% include 'members/member_list_superuser.inc' %} 22 | {% else %} 23 | {% include 'members/member_list.inc' %} 24 | {% endif %} 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /things/migrations/0008_alter_thingevent_kind.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-03-09 21:15 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('things', '0007_alter_thingevent_usage_seconds'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='thingevent', 16 | name='kind', 17 | field=models.CharField(choices=[('LOGIN', 'Login'), ('LOGOUT', 'Logout'), ('USAGE_MEMBER', 'Zeit (Member)'), ('USAGE_NONMEMBER', 'Zeit (Nicht-Member)'), ('USAGE_METALAB', 'Zeit (für Metalab)')], db_index=True, max_length=32), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /members/management/commands/list_intern_emails.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from members.models import MailinglistMail 4 | from members.models import get_mailinglist_members 5 | 6 | 7 | class Command(BaseCommand): 8 | def handle(self, *args, **kwargs): 9 | members_on_intern = get_mailinglist_members().filter(contactinfo__on_intern_list=True) 10 | addresses = [x.contactinfo.intern_list_email for x 11 | in members_on_intern 12 | if x.contactinfo.intern_list_email != ''] 13 | addresses.extend( 14 | e.email 15 | for e in MailinglistMail.objects.filter(on_intern_list=True) 16 | ) 17 | print('\n'.join(addresses)) 18 | -------------------------------------------------------------------------------- /sources/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | from django.db import models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name='WikiChange', 13 | fields=[ 14 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 15 | ('title', models.CharField(max_length=200)), 16 | ('link', models.CharField(max_length=300)), 17 | ('author', models.CharField(max_length=300)), 18 | ('updated', models.DateTimeField()), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | EXPOSE 8080 4 | 5 | ENV DEBIAN_FRONTEND noninteractive 6 | ENV PYTHONUNBUFFERED 1 7 | ENV DJANGO_STATIC_ROOT '/static' 8 | ENV DJANGO_MEDIA_ROOT '/media' 9 | ENV DJANGO_SETTINGS_MODULE: "mos.settings.deploy_env" 10 | 11 | RUN mkdir /code 12 | COPY . /code/ 13 | 14 | RUN apt-get update \ 15 | && apt-get install -y --force-yes libmariadb-dev libjpeg-dev netcat-traditional locales \ 16 | && pip3 install --no-cache-dir -vvv -r /code/requirements.txt \ 17 | && pip3 install --no-cache-dir -vvv -Ur /code/requirements-dev.txt 18 | 19 | RUN sed -i -e 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen && locale-gen 20 | 21 | WORKDIR /code 22 | ENTRYPOINT ["docker/entrypoint.sh"] 23 | -------------------------------------------------------------------------------- /members/migrations/0005_mailinglistmail.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-11-12 19:11 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0004_auto_20221112_1422'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='MailinglistMail', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('email', models.EmailField(max_length=254)), 19 | ('on_intern_list', models.BooleanField(default=True)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /templates/members/members_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block add_css %} 5 | 6 | {% endblock %} 7 | 8 | {% block hos_content %} 9 | 10 |

    all members

    11 |
    12 | 13 | {% if info_list %} 14 | {% for info_dict in info_list %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | {% endif %} 23 |
    {{ info_dict.member.membership_number }}{{ info_dict.user }}{{ info_dict.member.begin_of_membership }}{{ info_dict.kind }}
    24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | for i in $(seq 1 15); do 4 | nc -z "${MYSQL_HOST:=db}" "3306" > /dev/null 2>&1 && break 5 | echo "waiting for DB $MYSQL_HOST ($i)" 6 | sleep 1 7 | done 8 | 9 | if [ "$#" -ge "1" ]; then 10 | if [ "$1" = "run-dockerdev" ]; then 11 | set -x 12 | python3 manage.py migrate 13 | python3 manage.py runserver 0.0.0.0:8020 14 | else 15 | set -x 16 | python3 manage.py $@ 17 | fi 18 | else 19 | set -x 20 | python3 manage.py migrate || exit 1 # fail container if migration fails 21 | python3 manage.py collectstatic --no-input --clear --no-post-process -i "*.txt" -i "LICENSE" & 22 | daphne -b 0.0.0.0 -p ${DAPHNE_PORT:-3031} mos.asgi:application 23 | fi 24 | -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 | 5 | {% if form.errors %} 6 |

    Your username and password didn't match. Please try again.

    7 | {% endif %} 8 | 9 | Please enter your username and password to log in: 10 | 11 |
    {% csrf_token %} 12 | 13 | 14 | 15 |
    {{ form.username }}
    {{ form.password }}
    16 | 17 | 18 | 19 |
    20 | 21 |

    Forgot?

    22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /things/migrations/0002_auto_20240224_1354.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 13:54 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | import things.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('things', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='thing', 18 | name='token', 19 | field=models.CharField(default=things.models.make_token, max_length=128), 20 | ), 21 | migrations.AlterField( 22 | model_name='thing', 23 | name='name', 24 | field=models.CharField(max_length=64, unique=True), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /mos/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | 6 | import os 7 | 8 | import django 9 | from channels.auth import AuthMiddlewareStack 10 | from channels.routing import ProtocolTypeRouter 11 | from channels.routing import URLRouter 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.devel") 15 | django.setup() 16 | 17 | django_asgi_app = get_asgi_application() 18 | 19 | application = ProtocolTypeRouter({ 20 | # Django's ASGI application to handle traditional HTTP requests 21 | "http": django_asgi_app, 22 | "websocket": AuthMiddlewareStack( 23 | URLRouter([ 24 | ]) 25 | ), 26 | }) 27 | -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Category(models.Model): 5 | """ 6 | represents a Category (name, description) 7 | used in cal.models.Event 8 | """ 9 | slug = models.SlugField(null=True) 10 | name = models.CharField(max_length=30) 11 | description = models.CharField(max_length=200) 12 | 13 | def __str__(self): 14 | return self.name 15 | 16 | 17 | class Location(models.Model): 18 | """ 19 | represents a location(name, description, country) 20 | used in cal.models.Event 21 | """ 22 | name = models.CharField(max_length=30) 23 | description = models.CharField(max_length=200) 24 | country = models.CharField(max_length=100) 25 | 26 | def __str__(self): 27 | return self.name 28 | -------------------------------------------------------------------------------- /members/migrations/0021_communicationrecord_monthly_fee_at_contact_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-02-01 22:37 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("members", "0020_communicationrecord"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="communicationrecord", 16 | name="monthly_fee_at_contact", 17 | field=models.IntegerField(null=True), 18 | ), 19 | migrations.AddField( 20 | model_name="communicationrecord", 21 | name="outstanding_fees_at_contact", 22 | field=models.IntegerField(null=True), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /members/migrations/0022_contactinfo_in_intern_matrix_room_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-10-08 19:24 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0021_communicationrecord_monthly_fee_at_contact_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='contactinfo', 16 | name='in_intern_matrix_room', 17 | field=models.BooleanField(default=False), 18 | ), 19 | migrations.AddField( 20 | model_name='contactinfo', 21 | name='matrix_handle', 22 | field=models.CharField(blank=True, max_length=255), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /core/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def custom_settings_global(request): 5 | """ 6 | sets custom style and name as global template variables 7 | """ 8 | return {'custom_style': settings.HOS_CUSTOM_STYLE, 9 | 'HOS_NAME': settings.HOS_NAME, 10 | } 11 | 12 | 13 | def custom_settings_main(request): 14 | """ 15 | sets customizations specified in settings.py for the main page 16 | """ 17 | return {'introduction_text': settings.HOS_INTRODUCTION, 18 | 'members': settings.HOS_MEMBER_GALLERY, 19 | 'openlab': settings.HOS_OPENLAB, 20 | 'calendar': settings.HOS_CALENDAR, 21 | 'projects': settings.HOS_PROJECTS, 22 | 'recent_changes': settings.HOS_RECENT_CHANGES, 23 | } 24 | -------------------------------------------------------------------------------- /announce/templates/announce/write_message.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 |

    Metalab Announcer

    5 | 6 |

    7 | Diese Vorrichtung versendet Nachrichten an alle derzeit aktiven 8 | Mitglieder des Metalabs. Mißbrauch wird Folgen haben! 9 |

    10 |

    11 | Die folgenden Variablen können im Body verwendet werden:
    12 | {% verbatim %} 13 | {{username}} {{full_name}} {{short_name}} {{first_name}} {{last_name}} {{user_id}} {{profile_link}} {{IBAN}} {{BIC}} 14 | {% endverbatim %} 15 |

    16 | 17 |
    {% csrf_token %} 18 | 19 | {{ form.as_table }} 20 | 21 | 22 | 23 | 24 |
    Erst nachdenken, dann .
    25 |
    26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/things/thing_expiration_notice.mail: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hallo {{ user.username }}, 4 | 5 | Dein Zugang zur Nutzung von {{thing}} im metalab läuft am {{expiry_date}} ab, danach deaktiviert sich dein Zugang automatisch. 6 | 7 | Um deinen Zugang zu behalten, beachte bitte folgendes: 8 | {{expiry_notice}} 9 | 10 | Bitte beachte: Diese E-Mail wurde automatisch erstellt. 11 | 12 | Mit lieben Grüßen, 13 | Dein Metalab. 14 | 15 | -- 16 | Verein Metalab 17 | Rathausstraße 6 18 | 1010 Wien 19 | 20 | ZVR-Zahl: 269253896 21 | core@metalab.at 22 | 23 | https://metalab.at - 222m² Raum im Herzen Wiens für technologisch-kreative 24 | Projekte, Veranstaltungen, Software, Hardware,Essen & mehr... 25 | 26 | Schnappschüsse aus dem täglichen Leben rund ums Lab auf: 27 | Mastodon: https://chaos.social/@metalab 28 | Twitter: https://twitter.com/MetalabVie/ 29 | -------------------------------------------------------------------------------- /members/migrations/0025_alter_contactinfo_matrix_handle.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-11-23 14:37 2 | 3 | import django.core.validators 4 | from django.db import migrations 5 | from django.db import models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("members", "0024_alter_kindofmembership_options"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="contactinfo", 17 | name="matrix_handle", 18 | field=models.CharField( 19 | blank=True, 20 | max_length=255, 21 | validators=[ 22 | django.core.validators.RegexValidator( 23 | "\\A@[a-z0-9_.-]+:[a-z0-9_.-]+\\Z" 24 | ) 25 | ], 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /things/migrations/0005_auto_20240224_1447.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 14:47 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | import things.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('things', '0004_remove_thing_name'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='thing', 18 | name='slug', 19 | field=models.SlugField(help_text="Name of the thing, e.g. 'laser'", unique=True), 20 | ), 21 | migrations.AlterField( 22 | model_name='thing', 23 | name='token', 24 | field=models.CharField(default=things.models.make_token, help_text='auto generated, allows the machine to get the key IDs, KEEP SECRET', max_length=128), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /members/tests/test_membership_period.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.test import TestCase 4 | from freezegun import freeze_time 5 | 6 | from members.models import MembershipPeriod 7 | 8 | 9 | class MembershipPeriodTest(TestCase): 10 | def test_get_duration_takes_max_year(self): 11 | period = MembershipPeriod( 12 | begin=datetime.date(2000, 1, 1), 13 | end=datetime.date(9999, 12, 31) 14 | ) 15 | period.get_duration_in_month() # should not raise 16 | 17 | def test_get_duration_handles_future_end_date(self): 18 | with freeze_time('2000-05-10'): 19 | period = MembershipPeriod( 20 | begin=datetime.date(2000, 5, 1), 21 | end=datetime.date(3000, 5, 1) 22 | ) 23 | self.assertEqual(period.get_duration_in_month(), 1) 24 | -------------------------------------------------------------------------------- /templates/cal/calendar.inc: -------------------------------------------------------------------------------- 1 | {{ rendered_calendar }} 2 | {% if rendered_calendar %}
    {% endif %} 3 | 4 | {% with None as event %} 5 | {% load makeform %} 6 |
    7 | 10 |
      11 | {% for event in latestevents %} 12 | {% makeform event cal.forms.EventForm event_form %} 13 |
    • {% include "cal/eventinfo_nf.inc" with new=0 %}
    • 14 | {% endfor %} 15 |
    16 | 17 | {% makeform None cal.forms.EventForm event_form %} 18 | {% include "cal/eventinfo_nf.inc" with new=1 %} 19 | {% endwith %} 20 | {% if not all_events %} {% endif %} 21 |
    22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Metalab OS 2 | ========== 3 | 4 | About 5 | ----- 6 | 7 | This is the django project that runs our web site at the Metalab hackerspace 8 | in Vienna, Austria (https://metalab.at/). 9 | 10 | The official source repository and bug tracker as of 2012-03-21 can be 11 | found at: https://github.com/Metalab/mos . 12 | 13 | Development in this project will mainly focus on features that are useful to 14 | us locally. If you are interested in a "generalised" version, we are very much 15 | interested in talking to you! 16 | 17 | Contributors 18 | ------------ 19 | 20 | In no particular order and probably not complete. If you feel that you have 21 | been left out unfairly, please contact us. 22 | 23 | - fin 24 | - fhahn 25 | - enki 26 | - angelol 27 | - markus 28 | - stereotype 29 | - Stefan Kögl 30 | - Dražen Lučanin 31 | - Florian Schweikert 32 | -------------------------------------------------------------------------------- /members/migrations/0019_pendingpayment_creator.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-02-01 18:14 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ("members", "0018_remove_paymentinfo_bank_account_number_and_more"), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name="pendingpayment", 19 | name="creator", 20 | field=models.ForeignKey( 21 | null=True, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | related_name="created_pending_payments", 24 | to=settings.AUTH_USER_MODEL, 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /static/javascripts/formset_handler.js: -------------------------------------------------------------------------------- 1 | document.querySelectorAll("fieldset.module").forEach(function (e) { 2 | const header = e.querySelector("h2"); 3 | if (header) { 4 | const key = header.innerText; 5 | header.style = "cursor: pointer" 6 | function applyState() { 7 | header.innerHTML = (localStorage.getItem(key) ? '▸ ' : '▾ ') + key; 8 | e.querySelectorAll("div.form-row, table").forEach((element) => { 9 | if (!localStorage.getItem(key)) { 10 | element.classList.remove("hidden"); 11 | } else { 12 | element.classList.add("hidden"); 13 | } 14 | }); 15 | } 16 | applyState(); 17 | header.addEventListener("click", (event) => { 18 | if (localStorage.getItem(key)) { 19 | localStorage.removeItem(key); 20 | } else { 21 | localStorage.setItem(key, true); 22 | } 23 | applyState(); 24 | }); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /templates/auth/user_list.inc: -------------------------------------------------------------------------------- 1 |

    Public Users

    2 | 8 | 9 | {% if user.is_authenticated %} 10 |

    Users without Picture

    11 |
      {% for item in object_list %}{% if not item.contactinfo.image %} 12 |
    • {{ item.username }}
    • 13 | {% endif %}{% endfor %}
    {% endif %} 14 | -------------------------------------------------------------------------------- /templates/members/member_list.inc: -------------------------------------------------------------------------------- 1 |

    Public Members

    2 | 8 | 9 | {% if user.is_authenticated %} 10 |

    Users without Picture

    11 |
      {% for item in object_list %}{% if not item.contactinfo.image %} 12 |
    • {{ item.username }}
    • 13 | {% endif %}{% endfor %}
    {% endif %} 14 | -------------------------------------------------------------------------------- /members/migrations/0012_bankimportmatcher.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-09 20:06 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0011_locker'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='BankImportMatcher', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('matcher', models.CharField(max_length=80)), 19 | ('comment', models.CharField(max_length=200)), 20 | ('action', models.CharField(choices=[('drop', 'drop'), ('do_not_match', 'do not match'), ('color', 'color')], max_length=20)), 21 | ('color', models.CharField(blank=True, max_length=100, null=True)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /members/migrations/0003_auto_20221112_1308.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-11-12 13:08 2 | 3 | from django.db import migrations 4 | from django.db import models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('members', '0002_auto_20221112_1256'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='kindofmembership', 16 | name='fee_category', 17 | field=models.CharField(choices=[('standard', 'standard'), ('free', 'free'), ('decreased', 'ermäßigt'), ('increased', 'erhöht')], default='standard', max_length=9), 18 | ), 19 | migrations.AlterField( 20 | model_name='kindofmembership', 21 | name='spind', 22 | field=models.CharField(choices=[('no', '0 Spind'), ('small_1', '1 kleiner Spind'), ('big_1', '1 großer Spind'), ('small_2', '2 kleiner Spind')], default='no', max_length=7), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /projects/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.contrib.auth.models import User 4 | from django.db import models 5 | 6 | 7 | class ProjectManager(models.Manager): 8 | def get_queryset(self): 9 | return super().get_queryset().filter(deleted=False) 10 | 11 | 12 | class Project(models.Model): 13 | name = models.CharField(max_length=200) 14 | teaser = models.TextField(max_length=200, blank=True, null=True) 15 | wikiPage = models.CharField(max_length=200, blank=True, null=True) 16 | 17 | created_at = models.DateTimeField(default=datetime.datetime.now) 18 | created_by = models.ForeignKey(User, on_delete=models.CASCADE) 19 | finished_at = models.DateField(blank=True, null=True) 20 | deleted = models.BooleanField(default=False) 21 | 22 | objects = models.Manager() 23 | all = ProjectManager() 24 | 25 | def __str__(self): 26 | return self.name 27 | 28 | def delete(self): 29 | self.deleted = True 30 | self.save() 31 | -------------------------------------------------------------------------------- /projects/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.urls import re_path 3 | from django.views.generic import DetailView 4 | from django.views.generic import ListView 5 | 6 | import projects.views 7 | 8 | from .models import Project 9 | 10 | urlpatterns = [ 11 | path('', 12 | ListView.as_view( 13 | queryset=Project.all.all().order_by('-created_at')[:5], 14 | context_object_name="latestprojects", 15 | template_name="projects/project_archive.html", 16 | ), 17 | ), 18 | re_path( 19 | r'^(?P\d+)/delete/$', 20 | projects.views.delete_project, 21 | ), 22 | re_path( 23 | r'^(?P\d+)/$', 24 | DetailView.as_view( 25 | queryset=Project.all.all() 26 | ), 27 | ), 28 | re_path( 29 | r'^(?P\d+)/update/$', 30 | projects.views.update_project, 31 | ), 32 | path( 33 | 'new/', 34 | projects.views.update_project, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /cal/templatetags/cal_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template import Variable 3 | 4 | from cal.models import Event 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.tag(name="events_by_type") 10 | def do_events_by_type(parser, token): 11 | try: 12 | # split_contents() knows not to split quoted strings. 13 | tag_name, name = token.split_contents() 14 | except ValueError: 15 | raise template.TemplateSyntaxError("%r tag requires exactly two arguments" % token.contents.split()[0]) 16 | return EventsByTypeNode(name) 17 | 18 | 19 | class EventsByTypeNode(template.Node): 20 | def __init__(self, name): 21 | self.obj = Variable(name) 22 | 23 | def render(self, context): 24 | kw = self.obj.resolve(context).__class__.__name__.lower() + '__name' 25 | filter_arg = {str(kw): self.obj.resolve(context).name} 26 | obj_sub_list = Event.objects.filter(**filter_arg) 27 | context['latestevents'] = obj_sub_list 28 | return '' 29 | -------------------------------------------------------------------------------- /core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | from django.db import models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name='Category', 13 | fields=[ 14 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 15 | ('name', models.CharField(max_length=30)), 16 | ('description', models.CharField(max_length=200)), 17 | ], 18 | ), 19 | migrations.CreateModel( 20 | name='Location', 21 | fields=[ 22 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 23 | ('name', models.CharField(max_length=30)), 24 | ('description', models.CharField(max_length=200)), 25 | ('country', models.CharField(max_length=100)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /templates/auth/user_list_superuser.inc: -------------------------------------------------------------------------------- 1 |

    Metalab Mitglieder

    2 | 3 | {% for item in object_list %} 4 | 5 | 12 | 17 | 24 | 25 | {% endfor %} 26 |
    6 | {% if item.contactinfo.image %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | 13 | {{ item }}
    14 | member since: {{ item.contactinfo.get_date_of_first_join|date }}
    15 | {# debts: {{ item.contactinfo.get_debts }} Euro #} 16 |
    18 | edit
    19 | {% if item.contactinfo.get_wikilink %} 20 | wikiprofile
    21 | {% endif %} 22 | profile 23 |
    27 | -------------------------------------------------------------------------------- /members/migrations/0014_auto_20230709_2019.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-09 20:19 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('members', '0013_alter_bankimportmatcher_comment'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='bankimportmatcher', 19 | name='member', 20 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 21 | ), 22 | migrations.AlterField( 23 | model_name='bankimportmatcher', 24 | name='action', 25 | field=models.CharField(choices=[('drop', 'drop'), ('do_not_match', 'do not match'), ('match_to', 'match to'), ('color', 'color')], max_length=20), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /members/migrations/0004_auto_20221112_1422.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-11-12 14:22 2 | 3 | import django.core.validators 4 | from django.db import migrations 5 | from django.db import models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('members', '0003_auto_20221112_1308'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='contactinfo', 17 | name='key_id', 18 | field=models.CharField(blank=True, max_length=15, null=True, validators=[django.core.validators.RegexValidator('\\b[0-9]{2}-[0-9a-zA-Z]{12}\\b', 'iButton ID entspricht nicht dem Format [0-9]{2}-[0-9a-zA-Z]{12}')]), 19 | ), 20 | migrations.AlterField( 21 | model_name='kindofmembership', 22 | name='spind', 23 | field=models.CharField(choices=[('no', '0 Spind (0€)'), ('small_1', '1 kleiner Spind (8€)'), ('big_1', '1 großer Spind (10€)'), ('small_2', '2 kleiner Spind (16€)')], default='no', max_length=7), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /members/migrations/0011_locker.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-08 22:54 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('members', '0010_alter_paymentinfo_bank_account_iban'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Locker', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=80)), 22 | ('comment', models.TextField(blank=True)), 23 | ('price', models.IntegerField()), 24 | ('rented_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /mos/urls.py: -------------------------------------------------------------------------------- 1 | import django.views.i18n 2 | from django.conf import settings 3 | from django.conf.urls.static import static 4 | from django.contrib import admin 5 | from django.urls import include 6 | from django.urls import path 7 | 8 | import web.views 9 | from cal.feeds import EventFeed 10 | 11 | admin.autodiscover() 12 | 13 | urlpatterns = [ 14 | path('admin/jsi18n/', django.views.i18n.JavaScriptCatalog.as_view()), 15 | path('admin/', admin.site.urls), 16 | 17 | 18 | path('feeds/events/', EventFeed()), 19 | 20 | path('calendar/', include('cal.urls')), 21 | path('project/', include('projects.urls')), 22 | path('member/', include('members.urls')), 23 | path('announce/', include('announce.urls')), 24 | path('things/', include('things.urls')), 25 | path('cellardoor/', web.views.display_cellardoor), 26 | path('spaceapi.json', web.views.spaceapi), 27 | path('', web.views.display_main_page), 28 | path('mos', web.views.display_main_page), 29 | ] 30 | 31 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 32 | -------------------------------------------------------------------------------- /mos/settings/devel.py: -------------------------------------------------------------------------------- 1 | # Django settings for a local development instance of MOS 2 | from .common import * # NOQA 3 | # Make this unique, and don't share it with anybody. 4 | # ATTENTION - It may trigger an error or overwrite the SECRET_KEY if you develope in a docker environment with DEVEL settings 5 | from .secret_key import * # NOQA 6 | 7 | DEBUG = True 8 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': 'mos.sqlite', 14 | 'USER': '', 15 | 'PASSWORD': '', 16 | 'HOST': '', 17 | 'PORT': '', 18 | } 19 | } 20 | 21 | INSTALLED_APPS = INSTALLED_APPS + ( 22 | 'debug_toolbar',) 23 | 24 | MIDDLEWARE += ( 25 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 26 | ) 27 | 28 | MEDIA_ROOT = BASE_DIR.child("media") 29 | 30 | HOS_SEPA_CREDITOR_ID = 'AT29HXR00000037632' 31 | 32 | HOS_WIKI_URL = "https://metalab.at/wiki/" 33 | MEDIAWIKI_API = HOS_WIKI_URL + "api.php" 34 | 35 | ALLOWED_HOSTS = [ 36 | "*", 37 | ] 38 | -------------------------------------------------------------------------------- /members/forms.py: -------------------------------------------------------------------------------- 1 | import django.forms as forms 2 | from django.contrib.auth.models import User 3 | from django.forms.models import ModelForm 4 | 5 | from .models import ContactInfo 6 | 7 | 8 | class UserNameForm(ModelForm): 9 | class Meta: 10 | model = User 11 | fields = ('first_name', 'last_name') 12 | 13 | 14 | class UserInternListForm(ModelForm): 15 | class Meta: 16 | model = ContactInfo 17 | fields = ('on_intern_list', 'intern_list_email') 18 | 19 | 20 | class UserInternMatrixForm(ModelForm): 21 | class Meta: 22 | model = ContactInfo 23 | fields = ('in_intern_matrix_room', 'matrix_handle') 24 | 25 | 26 | class UserEmailForm(ModelForm): 27 | email = forms.EmailField(required=True) 28 | 29 | class Meta: 30 | model = User 31 | fields = ('email', ) 32 | 33 | 34 | class UserAdressForm(ModelForm): 35 | class Meta: 36 | model = ContactInfo 37 | fields = ('street', 'city', 'postcode', 'country') 38 | 39 | 40 | class UserImageForm(ModelForm): 41 | class Meta: 42 | model = ContactInfo 43 | fields = ('image', ) 44 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mos: 5 | image: metalab-mos:latest 6 | build: 7 | context: .. 8 | dockerfile: docker/Dockerfile 9 | command: run-dockerdev 10 | environment: 11 | PYTHONUNBUFFERED: 0 12 | DJANGO_SETTINGS_MODULE: "mos.settings.deploy_env" 13 | DJANGO_DEBUG: 'true' 14 | DJANGO_DOMAIN: 'localhost' 15 | MYSQL_DATABASE: 'mos' 16 | MYSQL_USER: 'mos' 17 | MYSQL_PASSWORD: 'mos' 18 | MYSQL_HOST: 'db' 19 | DJANGO_SECRET_KEY: 'CHANGEME' 20 | DJANGO_STATIC_ROOT: '/static' 21 | DJANGO_MEDIA_ROOT: '/media' 22 | # HOS_SEPA_CREDITOR_ID 23 | links: 24 | - db 25 | depends_on: 26 | - db 27 | volumes: 28 | - ..:/code 29 | - ./volume-static:/static 30 | - ./volume-media:/media 31 | ports: 32 | - "127.0.0.1:8020:8020" 33 | db: 34 | image: mariadb:10.5 35 | environment: 36 | MYSQL_DATABASE: 'mos' 37 | MYSQL_USER: 'mos' 38 | MYSQL_PASSWORD: 'mos' 39 | MYSQL_ROOT_PASSWORD: mostest 40 | restart: on-failure 41 | volumes: 42 | - ./volume-mysql:/var/lib/mysql 43 | -------------------------------------------------------------------------------- /mos/settings/deploy.py.tpl: -------------------------------------------------------------------------------- 1 | # Django settings for a deployed instance of MOS 2 | from common import * # NOQA 3 | 4 | DEBUG = False 5 | 6 | ADMINS = ( 7 | # ('MOS Admin', 'mos@metalab.at'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | USE_X_FORWARDED_HOST = True 13 | ALLOWED_HOSTS = ['metalab.at'] 14 | SESSION_COOKIE_DOMAIN = 'metalab.at' 15 | 16 | # Enable this if you are running behind a reverse proxy. 17 | # You MUST configure the proxy to strip X-Forwarded-Proto to avoid security 18 | # issues! This is how you do it in Apache (enable mod_headers): 19 | # 20 | # RequestHeader unset X-Forwarded-Proto 21 | # RequestHeader set X-Forwarded-Proto https env=HTTPS 22 | # 23 | #SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 24 | 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.mysql', 28 | 'NAME': 'mos', 29 | 'USER': 'mos', 30 | 'PASSWORD': '********', 31 | 'HOST': '', 32 | 'PORT': '', 33 | } 34 | } 35 | 36 | STATIC_ROOT = BASE_DIR.parent.child("www", "static") 37 | MEDIA_ROOT = BASE_DIR.parent.child("www", "media") 38 | 39 | HOS_SEPA_CREDITOR_ID = 'AT29HXR00000037632' 40 | -------------------------------------------------------------------------------- /projects/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | from django.db import models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Project', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('name', models.CharField(max_length=200)), 20 | ('teaser', models.TextField(max_length=200, null=True, blank=True)), 21 | ('wikiPage', models.CharField(max_length=200, null=True, blank=True)), 22 | ('created_at', models.DateTimeField(default=datetime.datetime.now)), 23 | ('finished_at', models.DateField(null=True, blank=True)), 24 | ('deleted', models.BooleanField(default=False)), 25 | ('created_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /sources/management/commands/get_wiki_changes.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import feedparser 4 | from django.conf import settings 5 | from django.core.management.base import BaseCommand 6 | from django.core.management.base import CommandError 7 | 8 | from ...models import WikiChange 9 | 10 | 11 | class Command(BaseCommand): 12 | help = 'Get the n most recent changes from the wiki.' 13 | 14 | def handle(self, *args, **options): 15 | feed = feedparser.parse(settings.MOS_WIKI_CHANGE_URL) 16 | 17 | # If there was _any_ kind of error, bail 18 | if 'bozo_exception' in feed: 19 | raise CommandError('Error parsing feed: %s' % feed['bozo_exception']) 20 | 21 | new_ids = [] 22 | for entry in feed.entries[:settings.MOS_WIKI_KEEP]: 23 | o, _ = WikiChange.objects.get_or_create( 24 | title=entry.title, 25 | link=entry.link, 26 | author=entry.author, 27 | updated=datetime(*entry.updated_parsed[:6]) 28 | ) 29 | new_ids.append(o.id) 30 | 31 | # Keep only the newly added entries 32 | WikiChange.objects.exclude(id__in=new_ids).delete() 33 | -------------------------------------------------------------------------------- /templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Metalab - Password change{% endblock %} 3 | 4 | {% block hos_content %} 5 | 6 |

    Password change

    7 | 8 |

    Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly.

    9 |
    10 |
    {% csrf_token %} 11 | {% if form.old_password.errors %} 12 |
    Error:
    {{ form.old_password.html_error_list }}
    13 | {% endif %} 14 |
    15 |
    {{ form.old_password }}
    16 | {% if form.new_password1.errors %}
    Error:
    {{ form.new_password1.html_error_list }}
    {% endif %} 17 |
    {{ form.new_password1 }}
    18 | {% if form.new_password2.errors %}
    Error:
    {{ form.new_password2.html_error_list }}
    {% endif %} 19 |
    {{ form.new_password2 }}
    20 |
    Submit
    21 | 22 |
    23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/jour_fixe_reminder.mail: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% language 'de' %}Liebe Leute, 2 | 3 | Am {{ jf.startDate | date:"l" }} den {{ jf.startDate | date:"Y-m-d" }} um {{ jf.startDate | date:"H:i" }} findet das nächste Jour Fixe statt. 4 | 5 | {% if wiki.article_missing %} 6 | Leider gibt es dazu keinen Wiki-Artikel. Das bedeutet wahrscheinlich, dass der 7 | Jour Fixe ausfällt, außer es werden heute noch Themen eingetragen. 8 | {% elif wiki.error %} 9 | Leider hat der Wiki-Artikel das falsche Format und kann nicht geparst werden, 10 | oder es ist ein anderer Fehler beim lesen des Artikels aufgetreten. Wenn das MOS 11 | den Artikel nicht lesen kann, kannst du ihn wahrscheinlich auch nicht lesen ;) 12 | Du kannst es aber versuchen. 13 | {% else %}Folgende Themen wurden bisher im Wiki eingetragen: 14 | {% for heading in wiki.headlines %} 15 | * {{ heading }}{% endfor %} 16 | 17 | Themen für das Jour-Fixe müssen mindestens 3 Tage vor dem Termin eingetragen werden. 18 | Wenn du also Themen hast, die besprochen gehören, kannst du sie noch heute eintragen. 19 | Sollten schon viele Themen eingetragen sein, warte eventuell bis zum nächsten Jour Fixe. 20 | {% endif %} 21 | Wiki Page: https://metalab.at/wiki/{{ jf.wikiPage }} 22 | 23 | <3 dein MOS{% endlanguage %} 24 | -------------------------------------------------------------------------------- /templates/members/members_history.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block add_css %} 4 | 5 | {% endblock %} 6 | 7 | {% block hos_content %} 8 | {% if list %} 9 |

    {{ HOS_MAME }} member history

    10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for l in list %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 |

    month

    number of member

    +

    -

    {{ l.month.month }}/{{ l.month.year }}

    {{ l.num_member }}

    {{ l.new_member }}

    {{ l.resigned_member }}

    28 | {% else %} 29 |

    no member in database

    30 | {% endif %} 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /things/migrations/0003_auto_20240224_1415.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 14:15 2 | 3 | import django.db.models.deletion 4 | import django.utils.timezone 5 | from django.conf import settings 6 | from django.db import migrations 7 | from django.db import models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('things', '0002_auto_20240224_1354'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='thinguser', 20 | name='best_before', 21 | field=models.DateField(blank=True, help_text='Wann die Schulung wiederholt werden sollte', null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='thinguser', 25 | name='created_at', 26 | field=models.DateField(auto_now_add=True, default=django.utils.timezone.now), 27 | preserve_default=False, 28 | ), 29 | migrations.AlterField( 30 | model_name='thinguser', 31 | name='user', 32 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='thingusers', to=settings.AUTH_USER_MODEL), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /members/migrations/0015_auto_20230709_2031.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-09 20:31 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('members', '0014_auto_20230709_2019'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='bankimportmatcher', 19 | name='color', 20 | field=models.CharField(blank=True, help_text="if action=color, e.g. 'red' or 'rgba(255,0,0,0.1)'", max_length=100, null=True), 21 | ), 22 | migrations.AlterField( 23 | model_name='bankimportmatcher', 24 | name='matcher', 25 | field=models.CharField(help_text='match in IBAN, sender, text', max_length=80), 26 | ), 27 | migrations.AlterField( 28 | model_name='bankimportmatcher', 29 | name='member', 30 | field=models.ForeignKey(blank=True, help_text='if action=match_to', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /members/migrations/0008_pendingpayment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-08 15:04 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('members', '0007_alter_contactinfo_image'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='PendingPayment', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('amount', models.FloatField()), 22 | ('comment', models.CharField(blank=True, max_length=200)), 23 | ('date', models.DateField()), 24 | ('method', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.paymentmethod')), 25 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | options={ 28 | 'ordering': ['date'], 29 | 'abstract': False, 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /things/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 13:48 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Thing', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('slug', models.SlugField(unique=True)), 23 | ('name', models.TextField(unique=True)), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='ThingUser', 28 | fields=[ 29 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('thing', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='things.thing')), 31 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /things/migrations/0006_thingevent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-24 16:54 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('things', '0005_auto_20240224_1447'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ThingEvent', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('kind', models.CharField(choices=[('LOGIN', 'Login'), ('LOGOUT', 'Logout'), ('USAGE_MEMBER', 'Zeit (Member)'), ('USAGE_NONMEMBER', 'Zeit (Nicht-Member)')], db_index=True, max_length=32)), 22 | ('usage_seconds', models.PositiveIntegerField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('thing', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='things.thing')), 25 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='thingevents', to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /members/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include 2 | from django.urls import path 3 | from django.urls import re_path 4 | from django.views.generic.list import ListView 5 | 6 | import members.views 7 | 8 | from .models import get_active_members 9 | 10 | username_patterns = [ 11 | path(r'update/userpic/', members.views.members_update_userpic), 12 | re_path(r'^update/(?P\w+)/$', members.views.members_update), 13 | path('', members.views.members_details), 14 | ] 15 | 16 | urlpatterns = [ 17 | path('', 18 | ListView.as_view( 19 | queryset=get_active_members().prefetch_related("contactinfo"), 20 | template_name='members/member_list.html', 21 | ), 22 | ), 23 | 24 | path('', include('django.contrib.auth.urls')), 25 | 26 | re_path(r'^valid_user/?$', members.views.valid_user), 27 | path('history/', members.views.members_history), 28 | path('hetti/', members.views.hetti), 29 | path('bank/', members.views.members_bank), 30 | path('bank/json/import', members.views.members_bank_json_import), 31 | path('bank/json/match', members.views.members_bank_json_match), 32 | path('keylist/', members.views.members_key_list), 33 | path('internlist/', members.views.members_intern_list), 34 | 35 | re_path(r'^(?P([\w\-+.@_])+)/', include(username_patterns)), 36 | ] 37 | -------------------------------------------------------------------------------- /static/stylesheets/members.css: -------------------------------------------------------------------------------- 1 | ul.user-list-pictures { 2 | max-width: 885px; 3 | margin-left: auto; 4 | margin-right: auto; 5 | } 6 | 7 | ul.user-list-pictures li { 8 | width: 220px; 9 | height: 220px; 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | float: left; 14 | border: 1px solid grey; 15 | border-left: none; 16 | border-top: none 17 | } 18 | ul.user-list-pictures li { 19 | border: 1px solid grey; 20 | text-align: center; 21 | margin: 0 -1px -1px 0 22 | } 23 | 24 | h2 { 25 | clear: both; 26 | padding-top: 2em; 27 | } 28 | 29 | td.changepwd { 30 | text-align: center; 31 | } 32 | 33 | td.graphbar { 34 | vertical-align: middle; 35 | } 36 | 37 | img.thumb_with_text { max-width:96px; max-height:96px; } 38 | 39 | #user_details { 40 | width: 100%; 41 | max-width: 700px; 42 | } 43 | 44 | #user_avatar { 45 | float:left; 46 | margin: 0 1em 1em 0; 47 | } 48 | 49 | #user_avatar { 50 | text-align: center; 51 | } 52 | 53 | #user_avatar > img { 54 | width: 80px; 55 | } 56 | 57 | #user_paymenthistory tr:first-child td { 58 | font-weight: bold; 59 | text-align: center; 60 | } 61 | 62 | .hetti span { 63 | display: block; 64 | font-size: 80%; 65 | } 66 | 67 | .jsonimport td { 68 | border-width: 1px; 69 | border-style: solid; 70 | border-color: black; 71 | } 72 | -------------------------------------------------------------------------------- /static/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | /* ------------------------- 2 | by Eric Meyer 3 | 4 | www.meyerweb.com 5 | ---------------------------*/ 6 | 7 | html, body, div, span, applet, object, iframe, 8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 9 | a, abbr, acronym, address, big, cite, code, 10 | del, dfn, em, font, img, ins, kbd, q, s, samp, 11 | small, strike, strong, sub, sup, tt, var, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td { 15 | margin: 0; 16 | padding: 0; 17 | border: 0; 18 | outline: 0; 19 | font-weight: inherit; 20 | font-style: inherit; 21 | font-size: 100%; 22 | font-family: inherit; 23 | vertical-align: baseline; 24 | } 25 | /* remember to define focus styles! */ 26 | /* does not validate -- fin @ 2008-01-04 */ 27 | /*:focus { 28 | outline: 0; 29 | }*/ 30 | body { 31 | line-height: 1em; 32 | color: black; 33 | background: white; 34 | } 35 | ol, ul { 36 | list-style: none; 37 | } 38 | /* tables still need 'cellspacing="0"' in the markup */ 39 | table { 40 | border-collapse: separate; 41 | border-spacing: 0; 42 | } 43 | caption, th, td { 44 | text-align: left; 45 | font-weight: normal; 46 | } 47 | blockquote:before, blockquote:after, 48 | q:before, q:after { 49 | content: ""; 50 | } 51 | blockquote, q { 52 | quotes: "" ""; 53 | } 54 | -------------------------------------------------------------------------------- /templates/members/member_list_superuser.inc: -------------------------------------------------------------------------------- 1 |

    {{ HOS_NAME }} Members

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for item in object_list %} 14 | 15 | 22 | 25 | 28 | 35 | 38 | 41 | 42 | {% endfor %} 43 |
    namemember sincemonthly feedebts
    16 | {% if item.contactinfo.image %} 17 | 18 | {% else %} 19 | 20 | {% endif %} 21 | 23 | {{ item }}
    24 |
    26 | {{ item.contactinfo.get_date_of_first_join|date:"Y-m-d" }}
    27 |
    29 | {% if item.contactinfo.get_wikilink %} 30 | wikiprofile
    31 | {% endif %} 32 | profile 33 | {{item.first_name}} {{item.last_name}}
    34 |
    36 | {{ item.contactinfo.get_debt_for_this_month|floatformat:2 }} Euro 37 | 39 | {{ item.contactinfo.get_debts|floatformat:2 }} Euro 40 |
    44 | -------------------------------------------------------------------------------- /things/management/commands/send_thing_expiration_notices.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.conf import settings 4 | from django.core.mail import send_mail 5 | from django.core.management.base import BaseCommand 6 | from django.template.loader import get_template 7 | 8 | from things.models import ThingUser 9 | 10 | 11 | def mail(template, ctx_vars, recipient): 12 | tpl = get_template(template) 13 | body = tpl.render(ctx_vars) 14 | subject = get_template(template + ".subject").render(ctx_vars).strip() 15 | send_mail( 16 | subject, body, settings.HOS_ANNOUNCE_FROM, [recipient], fail_silently=False 17 | ) 18 | 19 | 20 | class Command(BaseCommand): 21 | help = "Send thing expiration reminder to all members whose thing expires 1 month from now" 22 | 23 | def handle(self, *args, **options): 24 | expiring_thing_users = ThingUser.objects.filter( 25 | best_before__lte=datetime.date.today() + datetime.timedelta(days=31), 26 | best_before__gt=datetime.date.today() + datetime.timedelta(days=30), 27 | ) 28 | 29 | for expiring_thing in expiring_thing_users: 30 | ctx = { 31 | "user": expiring_thing.user, 32 | "thing": expiring_thing.thing, 33 | "expiry_date": expiring_thing.best_before, 34 | "expiry_notice": expiring_thing.thing.expiry_notice, 35 | } 36 | mail("things/thing_expiration_notice.mail", ctx, expiring_thing.user.email) 37 | -------------------------------------------------------------------------------- /docs/THINGS.md: -------------------------------------------------------------------------------- 1 | # Things 2 | 3 | MOS allows admins to maintain a list of Things (=devices, machines, permissions, ...) 4 | and attach uses to them, e.g. user "ripper" is allowed to use thing "laser". 5 | Additionally, things may report being used, e.g. "ripper" used "laser" for 60 6 | seconds. 7 | 8 | ## Auth 9 | 10 | Most endpoints require authentication. Ask Vorstand for a token and provide it 11 | in the `X-TOKEN` header. 12 | 13 | ## Getting key IDs allowed to operate a Thing 14 | 15 | ``` 16 | $ curl https://metalab.at/things/keys/prusaxl -H "X-TOKEN: XXX" 17 | 00-000000000001,luto 18 | 00-000000000002,ripper 19 | ``` 20 | 21 | ## Reporting usage of a thing 22 | 23 | ``` 24 | $ curl https://metalab.at/things/usage/prusaxl -H "X-TOKEN: XXX" -XPOST -d 'user=luto&kind=LOGIN' 25 | ``` 26 | 27 | `user` is a mos user/member, identified by their name, see `/keys/` endpoint. 28 | 29 | `kind` may be: 30 | 31 | * `LOGIN`, user started using a machine 32 | * `LOGOUT`, user stopped using a machine 33 | * `USAGE_MEMBER`, user used the machine for X seconds, for a member 34 | * `USAGE_NONMEMBER`, user used the machine for X seconds, for a non-member 35 | * `USAGE_METALAB`, user used the machine for X seconds, for a metalab infra project 36 | 37 | Additionally, supply `usage_seconds=` as an integer for `USAGE_` kinds. 38 | 39 | ## Getting stats for a Thing 40 | 41 | ``` 42 | $ curl https://metalab.at/things/stats/laser 43 | [["2024-02-24", 1], ["2024-02-24", 1]] 44 | ``` 45 | 46 | Format: `[[date of usage, usage in seconds],...]`. 47 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for MOS project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "{{ project_name }}.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.devel") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | 29 | application = get_wsgi_application() 30 | 31 | # Apply WSGI middleware here. 32 | # from helloworld.wsgi import HelloWorldApplication 33 | # application = HelloWorldApplication(application) 34 | -------------------------------------------------------------------------------- /templates/cal/event_archive.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ HOS_NAME }} - Calendar{% endblock %} 4 | 5 | {% block add_css %} 6 | 7 | {% endblock %} 8 | 9 | {% block hos_content %} 10 | 15 | 16 | 17 | {% if type %} 18 |

    {{ type }} 19 | {% if title %} 20 | / {{ title }} / 21 | {% endif %} 22 | 23 | {% endif %} 24 | {% if description %} 25 | {{ description.description }}

    26 | {% endif %} 27 | 28 |
      29 | {% for date in date_list %} 30 |
    • {{date.year}}
    • 31 | {% endfor %} 32 |
    33 | 34 | {% if type %} 35 | {% with "True" as edit_disabled %} 36 | {% with "True" as all_events %} {# variable for switching more link ind calendar.inc #} 37 |
    38 | {% include "cal/calendar.inc" %} 39 |
    40 | {% endwith %} 41 | {% endwith %} 42 | {% else %} 43 | {% with "True" as all_events %} {# variable for switching more link ind calendar.inc #} 44 |
    45 | {% include "cal/calendar.inc" %} 46 |
    47 | {% endwith %} 48 | {% endif %} 49 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /templates/cal/event_archive_year.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ HOS_NAME }} - Calendar for {{year}}{% endblock %} 4 | 5 | {% block add_css %} 6 | 7 | {% endblock %} 8 | 9 | {% block hos_content %} 10 | 15 | 16 | 17 | {% if type %} 18 |

    {{ type }} 19 | {% if title %} 20 | / {{ title }} / 21 | {% endif %} 22 | 23 | {% endif %} 24 | {% if description %} 25 | {{ description.description }}

    26 | {% endif %} 27 | 28 | 32 | 33 | 34 | {% with object_list as latestevents %} 35 | 36 | {% if type %} 37 | {% with "True" as edit_disabled %} 38 | {% with "True" as all_events %} {# variable for switching more link ind calendar.inc #} 39 | {% include "cal/calendar.inc" %} 40 | {% endwith %} 41 | {% endwith %} 42 | {% else %} 43 | {% with "True" as all_events %} {# variable for switching more link ind calendar.inc #} 44 | {% include "cal/calendar.inc" %} 45 | {% endwith %} 46 | {% endif %} 47 | {% endwith %} 48 | 49 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /static/stylesheets/soup_startpage.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:0; 3 | padding:0; 4 | width:315px; 5 | overflow-x: hidden; 6 | 7 | /* from global.css */ 8 | font: 76% "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif; 9 | color: #444; 10 | line-height: 1.3em; 11 | 12 | 13 | } 14 | 15 | 16 | 17 | 18 | 19 | /* text formating from global.cs */ 20 | h1, h2, h3 { line-height: 1em;} 21 | h1 { font-size: 1.5em; color: #4F5C7F; margin: 0 0 1em 0;} 22 | h2 { font-size: 1.1em; color: #4F5C7F; margin: 1.5em 0 1em 0;} 23 | h3 { font-size: 1.2em; color: #4F5C7F; margin: 1.5em 0 1em 0;} 24 | p { margin: 1em 0;} 25 | a { text-decoration: none; color: #5D5EA2;} 26 | a:hover { color: #ffffff; background: #8c9ce9;} 27 | img { border:none;} 28 | 29 | 30 | div.imagecontainer { padding-bottom: 5px;} 31 | 32 | 33 | div.post_quote, div.post_regular, div.post_link, div.description { 34 | padding: 0 4px 0 4px; 35 | } 36 | 37 | 38 | div.post_quote cite {margin-left:50px} 39 | div.post_quote span.quote {text-style:italic} 40 | div.post_quote blockquote { 41 | font-style: italic; 42 | font-weight:bold; 43 | font-size:1.4em; 44 | } 45 | 46 | 47 | 48 | 49 | 50 | 51 | div.meta, #headercontainer { 52 | display:none; 53 | } 54 | 55 | 56 | div.post { 57 | 58 | border-bottom:4px groove black; 59 | padding-bottom:15px; 60 | margin-bottom:45px; 61 | } 62 | 63 | 64 | 65 | 66 | 67 | /* Quotes schaun auf der startseite ganz mies aus 68 | span.quote{display:none;} 69 | div.post_quote blockquote { 70 | font-size:1.0em; 71 | font-style:normal; 72 | font-weight:normal; 73 | padding:0; 74 | margin:0; 75 | } 76 | */ 77 | -------------------------------------------------------------------------------- /templates/members/members_hetti.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block add_css %} 4 | 5 | {% endblock %} 6 | 7 | {% block hos_content %} 8 |

    9 | HETTI 10 | (Hetti Equivalent Text 2 Table Interface) 11 |

    12 |
    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for month in months %} 23 | 24 | 25 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | {% endfor %} 41 |
    MonthFee kindsSpind kindsFees MembershipFees SpindFeesTotal Payments
    {{ month.month|date:"Y-m" }} 26 | {% for kind, count in month.fee_category_kinds.items %} 27 | {{ count }}x {{ kind }}
    28 | {% endfor %} 29 |
    31 | {% for kind, count in month.spind_kinds.items %} 32 | {{ count }}x {{ kind }}
    33 | {% endfor %} 34 |
    {{ month.total_fees_membership|floatformat:2 }} Euro{{ month.total_fees_spind|floatformat:2 }} Euro{{ month.total_fees|floatformat:2 }} Euro{{ month.total_payments|floatformat:2 }} Euro
    42 |

    Hint call me like this: /member/hetti/?start_date=2022-01-01&end_date=2022-08-01

    43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /members/management/commands/member_categories.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from datetime import date 3 | 4 | from django.core.management.base import LabelCommand 5 | from django.db.models import Q 6 | 7 | from ...models import get_active_members_for 8 | 9 | 10 | class Command(LabelCommand): 11 | help = "My shiny new management command." 12 | 13 | def handle_label(self, label, **options): 14 | year = int(label) 15 | 16 | print(f'Mitgliedskategorien je Monat im Jahr {year}:') 17 | for month in range(1, 13): 18 | dt = date(year, month, 1) 19 | data_dict, sum_users = self.handle_date(dt) 20 | print(dt.strftime('%m/%Y'), 'Member in Summe:', sum_users) 21 | for key, value in data_dict.items(): 22 | print(key, value) 23 | 24 | print("\n") 25 | 26 | def handle_date(self, dt): 27 | member_category_dict = defaultdict(int) 28 | user_sum = 0 29 | for user in get_active_members_for(dt): 30 | # we have to get first() because the last day of one period 31 | # may be the same as the first day of the next period. 32 | # this shouldn't happen, but oh well... 33 | # also get_active_members_for uses begin <= dt <= end 34 | period = user.membershipperiod_set.filter( 35 | Q(begin__lte=dt), 36 | Q(end__isnull=True) | Q(end__gte=dt) 37 | ).first() 38 | 39 | member_category_dict[period.kind_of_membership.name] += 1 40 | user_sum += 1 41 | 42 | return member_category_dict, user_sum 43 | -------------------------------------------------------------------------------- /core/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | django_playground.core.middleware 3 | inspired by www.pylucid.org 4 | see http://trac.pylucid.net/browser/trunk/pylucid/PyLucid/middlewares/\ 5 | pagestats.py for authors 6 | """ 7 | import time 8 | 9 | from django.db import connection 10 | from django.utils.deprecation import MiddlewareMixin 11 | from django.utils.encoding import force_str 12 | 13 | from core.utils import human_readable_time 14 | 15 | TAG = '' 16 | FOOTER_STAT_STRING = 'renderd in %(render_time)s - %(queries)s sql queries' 17 | 18 | 19 | class SetStatFooter(MiddlewareMixin): 20 | """ 21 | Sets some performance data (number of queries,.. 22 | """ 23 | 24 | def process_request(self, request): 25 | self.time_started = time.time() 26 | self.old_queries = len(connection.queries) 27 | 28 | def process_response(self, request, response): 29 | try: 30 | if 'text/html' not in response['Content-Type']: 31 | return response 32 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': 33 | return response 34 | if response.status_code != 200: 35 | return response 36 | 37 | queries = len(connection.queries) - self.old_queries 38 | 39 | render_time = human_readable_time(time.time() - self.time_started) 40 | stats = FOOTER_STAT_STRING % {'render_time': render_time, 41 | 'queries': queries} 42 | content = response.content 43 | response.content = force_str(content).replace(TAG, stats) 44 | except: 45 | pass 46 | 47 | return response 48 | -------------------------------------------------------------------------------- /core/templatetags/makeform.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.tag(name="makeform") 7 | def do_makeform(parser, token): 8 | """ do_makeform variable path.to.form.class target_name """ 9 | try: 10 | tag_name, context_var, form_path, target_name = token.split_contents() 11 | except ValueError: 12 | raise template.TemplateSyntaxError("%r tag requires two arguments" % token.contents.split()[0]) 13 | 14 | try: 15 | modulename, classname = form_path.rsplit('.', 1) 16 | module = __import__(modulename, fromlist=[classname]) 17 | except: 18 | raise template.TemplateSyntaxError("the path to form class (%s) should be import-able! %r" % (form_path.rsplit('.', 1)[0], token.contents.split()[0])) 19 | 20 | cls = getattr(module, classname) 21 | 22 | return MakeFormNode(context_var, cls, target_name) 23 | 24 | 25 | class MakeFormNode(template.Node): 26 | def __init__(self, context_var, cls, target_name): 27 | self.context_var = template.Variable(context_var) if context_var != 'None' else None 28 | self.cls = cls 29 | self.target_name = target_name 30 | 31 | def render(self, context): 32 | if self.context_var: 33 | try: 34 | actual_var = self.context_var.resolve(context) 35 | except template.VariableDoesNotExist: 36 | raise template.TemplateSyntaxError('MakeFormNode cannot resolve variable') 37 | 38 | if not self.context_var: 39 | context[self.target_name] = self.cls() 40 | else: 41 | context[self.target_name] = self.cls(instance=actual_var) 42 | 43 | return '' 44 | -------------------------------------------------------------------------------- /members/migrations/0020_communicationrecord.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-02-01 22:31 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ("members", "0019_pendingpayment_creator"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="CommunicationRecord", 19 | fields=[ 20 | ( 21 | "id", 22 | models.AutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("contacted_on", models.DateField()), 30 | ("initial_contact", models.BooleanField(default=False)), 31 | ( 32 | "contacted_by", 33 | models.CharField(blank=True, max_length=100, null=True), 34 | ), 35 | ("contact_resolved", models.BooleanField(default=False)), 36 | ("comment", models.CharField(blank=True, max_length=1000, null=True)), 37 | ( 38 | "user", 39 | models.ForeignKey( 40 | on_delete=django.db.models.deletion.CASCADE, 41 | to=settings.AUTH_USER_MODEL, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /projects/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import get_object_or_404 3 | from django.shortcuts import render 4 | from django.views.decorators.http import require_http_methods 5 | 6 | from .forms import ProjectForm 7 | from .models import Project 8 | 9 | 10 | @login_required 11 | def update_project(request, object_id=None): 12 | """ 13 | Updates or creates a project and returns a view with a project form. 14 | """ 15 | project = None if object_id is None else Project.all.get(id=object_id) 16 | 17 | if request.method == 'POST': 18 | project_form = ProjectForm(request.POST, instance=project) 19 | if project_form.is_valid(): 20 | project = project_form.save(commit=False) 21 | if project.created_by_id is None: 22 | project.created_by = request.user 23 | project.save() 24 | else: 25 | project_form = ProjectForm() 26 | 27 | return render(request, 'projects/projectinfo.inc', { 28 | 'project_form': project_form, 29 | 'project': project, 30 | }) 31 | 32 | 33 | @login_required 34 | @require_http_methods(['POST']) 35 | def delete_project(request, object_id=None): 36 | """Delete a project""" 37 | project = get_object_or_404(Project, id=object_id) 38 | project.delete() 39 | return _get_latest(request) 40 | 41 | 42 | def _get_latest(request, current_project=None, errors=None, 43 | e_project_name=None): 44 | """ Returns a view that displays the latest 5 projects """ 45 | 46 | latest = Project.all.order_by('-created_at')[:5] 47 | return render(request, 'projects/overview.inc', { 48 | 'project': current_project, 49 | 'latestprojects': latest, 50 | 'errors': errors, 51 | 'e_project_name': e_project_name, 52 | }) 53 | -------------------------------------------------------------------------------- /cal/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | from django.db import models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('core', '__first__'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Event', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('name', models.CharField(max_length=200)), 21 | ('teaser', models.TextField(max_length=200, null=True, blank=True)), 22 | ('wikiPage', models.CharField(max_length=200)), 23 | ('startDate', models.DateTimeField()), 24 | ('endDate', models.DateTimeField(null=True, blank=True)), 25 | ('who', models.CharField(max_length=200, blank=True)), 26 | ('where', models.CharField(max_length=200, blank=True)), 27 | ('created_at', models.DateTimeField(default=datetime.datetime.now)), 28 | ('deleted', models.BooleanField(default=False)), 29 | ('category', models.ForeignKey( 30 | blank=True, 31 | on_delete=models.CASCADE, 32 | to='core.Category', 33 | null=True, 34 | )), 35 | ('created_by', models.ForeignKey( 36 | to=settings.AUTH_USER_MODEL, 37 | on_delete=models.CASCADE, 38 | )), 39 | ('location', models.ForeignKey( 40 | blank=True, 41 | to='core.Location', 42 | on_delete=models.CASCADE, 43 | null=True, 44 | )), 45 | ], 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /templates/cal/eventinfo_nf.inc: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 5 | {% if not new %} 6 |

    7 | {{ event.startDate|date:"D d.m.Y H:i" }} {% if event.endDate %} - {% if event.start_end_date_eq %} {{ event.endDate|date:"H:i" }} {% else %} {{ event.endDate|date:"D d.m.Y H:i" }} {% endif %} {% endif %} 8 | {% if event.wikiPage %} {% endif %}{{ event.name }}{% if event.wikiPage %}{% endif %} 9 | {% if event.teaser %}{{ event.teaser }}{% endif %} 10 | {% if event.category %} | {{ event.category }}{% endif %} 11 | {% if event.location%} | {{ event.location }}{% endif %} 12 | 13 | {% if user.is_authenticated and not edit_disabled %} 14 | 15 | Edit 16 | 17 | {%endif %} 18 | 📅 ical 19 | {% if user.is_authenticated %} 20 | | created by {{ event.created_by }} 21 | {%endif %} 22 |

    23 | 24 | {%else%} 25 |

    Download current events in ical format

    26 | {% if user.is_authenticated and not edit_disabled %} 27 |

    28 | 29 | Create new event 30 | 31 |

    32 | {%endif %} 33 | {%endif %} 34 | 35 | 36 | {% if user.is_authenticated %} 37 | {% include "cal/event_form.inc" with from_nf=1 %} 38 | {% endif %} 39 |
    40 | -------------------------------------------------------------------------------- /things/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from import_export import resources 3 | from import_export.admin import ImportExportModelAdmin 4 | 5 | from .models import Thing 6 | from .models import ThingEvent 7 | from .models import ThingUser 8 | 9 | 10 | class ThingUserInline(admin.TabularInline): 11 | model = ThingUser 12 | 13 | 14 | class ThingResource(resources.ModelResource): 15 | 16 | class Meta: 17 | model = Thing 18 | fields = ( 19 | "slug", 20 | "token", 21 | ) 22 | 23 | 24 | @admin.register(Thing) 25 | class ThingAdmin(ImportExportModelAdmin): 26 | list_filter = [ 27 | 'slug', 28 | ] 29 | list_display = [ 30 | 'slug', 31 | ] 32 | inlines = [ 33 | ThingUserInline, 34 | ] 35 | resource_classes = [ThingResource] 36 | 37 | 38 | class ThingUserResource(resources.ModelResource): 39 | 40 | class Meta: 41 | model = ThingUser 42 | fields = ( 43 | 'thing__slug', 44 | 'user__username', 45 | 'created_at', 46 | 'best_before', 47 | ) 48 | 49 | 50 | @admin.register(ThingUser) 51 | class ThingUserAdmin(ImportExportModelAdmin): 52 | list_filter = [ 53 | 'thing', 54 | 'user', 55 | ] 56 | list_display = [ 57 | 'thing', 58 | 'user', 59 | 'created_at', 60 | 'best_before', 61 | ] 62 | resource_classes = [ThingUserResource] 63 | 64 | 65 | class ThingEventResource(resources.ModelResource): 66 | 67 | class Meta: 68 | model = ThingEvent 69 | fields = ( 70 | 'thing__slug', 71 | 'user__username', 72 | 'kind', 73 | 'created_at', 74 | 'usage_seconds', 75 | ) 76 | 77 | 78 | @admin.register(ThingEvent) 79 | class ThingEventAdmin(ImportExportModelAdmin): 80 | list_filter = [ 81 | 'thing', 82 | 'user', 83 | 'kind', 84 | ] 85 | list_display = [ 86 | 'thing', 87 | 'user', 88 | 'kind', 89 | 'created_at', 90 | ] 91 | resource_classes = [ThingEventResource] 92 | -------------------------------------------------------------------------------- /things/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | from django.utils.crypto import get_random_string 4 | 5 | 6 | def make_token(): 7 | return get_random_string(42) 8 | 9 | 10 | class Thing(models.Model): 11 | """Something in the hackspace that people can get permission to use, e.g. the laser or the door""" 12 | 13 | slug = models.SlugField(unique=True, help_text="Name of the thing, e.g. 'laser'") 14 | token = models.CharField( 15 | max_length=128, 16 | default=make_token, 17 | help_text="auto generated, allows the machine to get the key IDs, KEEP SECRET", 18 | ) 19 | expiry_notice = models.TextField( 20 | default="", help_text="Expiry notice text sent with the expiry notice email" 21 | ) 22 | 23 | def __str__(self): 24 | return "Thing " + self.slug 25 | 26 | 27 | class ThingUser(models.Model): 28 | """Allows a user to use a thing, e.g. user ripper has permission to use the laser""" 29 | 30 | thing = models.ForeignKey(Thing, on_delete=models.PROTECT) 31 | user = models.ForeignKey( 32 | User, 33 | on_delete=models.CASCADE, 34 | related_name="thingusers", 35 | ) 36 | created_at = models.DateField(auto_now_add=True) 37 | best_before = models.DateField( 38 | help_text="Wann die Schulung wiederholt werden sollte", 39 | null=True, 40 | blank=True, 41 | ) 42 | 43 | 44 | class ThingEvent(models.Model): 45 | class Kind(models.TextChoices): 46 | LOGIN = ("LOGIN", "Login") 47 | LOGOUT = ("LOGOUT", "Logout") 48 | USAGE_MEMBER = ("USAGE_MEMBER", "Zeit (Member)") 49 | USAGE_NONMEMBER = ("USAGE_NONMEMBER", "Zeit (Nicht-Member)") 50 | USAGE_METALAB = ("USAGE_METALAB", "Zeit (für Metalab)") 51 | 52 | thing = models.ForeignKey(Thing, on_delete=models.PROTECT) 53 | user = models.ForeignKey( 54 | User, 55 | on_delete=models.CASCADE, 56 | related_name="thingevents", 57 | ) 58 | kind = models.CharField( 59 | max_length=32, 60 | db_index=True, 61 | choices=Kind.choices, 62 | ) 63 | usage_seconds = models.PositiveIntegerField( 64 | blank=True, 65 | null=True, 66 | ) 67 | created_at = models.DateTimeField(auto_now_add=True) 68 | -------------------------------------------------------------------------------- /static/stylesheets/cellardoor.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:0; 3 | padding:0; 4 | background-image:url(metanight.png); 5 | background-repeat:no-repeat; 6 | background-color:#00174D; 7 | font-family:sans-serif; 8 | overflow:hidden; 9 | } 10 | 11 | 12 | #container { 13 | width:1280px; 14 | height:800px; 15 | overflow:hidden; 16 | } 17 | 18 | #eventliste { 19 | width:400px; 20 | position:absolute; 21 | top:60px; 22 | left:891px; 23 | } 24 | 25 | #event_anounce { 26 | display:none; 27 | font-size:32px; 28 | font-family: Times; 29 | padding-left:90px; 30 | height:35px; 31 | } 32 | 33 | #event_anounce .fg { 34 | color:#F36523; 35 | position:relative; 36 | top:-37px; 37 | right:1px; 38 | } 39 | #event_anounce .bg {positon:relative;} 40 | 41 | 42 | a { 43 | color:#000; 44 | text-decoration:none; 45 | font-size:21px; 46 | } 47 | 48 | 49 | /* styling */ 50 | 51 | #calendar-content #calendar-content { 52 | background-color: #003151; 53 | padding:10px 20px 7px 10px; 54 | -moz-border-radius: 10px; 55 | } 56 | 57 | #calendar-content ul { 58 | list-style-position: inline; 59 | padding:0; 60 | } 61 | 62 | #calendar-content li { 63 | display: block; 64 | } 65 | 66 | 67 | 68 | #calendar-content li p { 69 | margin: 0; 70 | padding: 0.5em 1em; 71 | line-height:1.15em; 72 | } 73 | 74 | 75 | #calendar-content li .event { 76 | /*background: #A7B9C3;*/ 77 | background: #fff; 78 | 79 | margin-bottom:3px; 80 | -moz-border-radius: 5px; 81 | -webkit-border-radius: 5px; 82 | 83 | 84 | } 85 | 86 | #statusInfo { display: none; } 87 | 88 | #calendar-content li .past_event { 89 | /* background: #F36523; */ 90 | background:#fa6232; 91 | color:#fff; 92 | } 93 | #calendar-content li .past_event a { color:#fff; } 94 | 95 | #calendar-content .event_date, p.event 96 | { 97 | font-weight: bold; 98 | padding-right: 5px; 99 | margin-right: 5px; 100 | } 101 | 102 | 103 | span.teaser { 104 | display:block; 105 | padding-top:2px; 106 | } 107 | 108 | span.event_date { 109 | font-size:14px; 110 | font-weight:normal; 111 | display:block; 112 | margin-bottom:0.4em; 113 | } 114 | 115 | 116 | #calender_morelink, span.location, span.category, .invisible, a.hoverHidden, span.hoverHidden, div#calendarcontainer {display:none} 117 | -------------------------------------------------------------------------------- /cal/forms.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import django.forms as forms 4 | import requests 5 | from django.contrib.admin.widgets import AdminSplitDateTime 6 | from django.forms import ModelForm 7 | from django.forms.fields import SplitDateTimeField 8 | 9 | from .models import Event 10 | 11 | 12 | class EventForm(ModelForm): 13 | """ 14 | Form to add an event 15 | """ 16 | 17 | startDate = SplitDateTimeField(widget=AdminSplitDateTime) 18 | endDate = SplitDateTimeField(required=False, widget=AdminSplitDateTime) 19 | teaser = forms.CharField(required=False) 20 | 21 | class Meta: 22 | model = Event 23 | exclude = ('where', 'created_at', 'created_by', 'deleted', 'who') 24 | 25 | def clean_wiki_url_fields(self, cleaned_data, field): 26 | if cleaned_data.get(field): 27 | category = cleaned_data.get('category') 28 | wikipage, _ = re.subn(r'(^http(s)://metalab.at/wiki/|\.\.|\ |\%|\&)', '', cleaned_data.get(field), 200) 29 | cleaned_data[field] = wikipage 30 | if cleaned_data.get('advertise') and re.match(r'^(Benutzer(in)?|User):', wikipage): 31 | self.add_error(field, 'Userpages don\'t provide adequate information for public Events') 32 | 33 | r = requests.get('https://metalab.at/wiki/%s' % wikipage) 34 | 35 | if r.status_code == 404 and (not category or category.name != "jour fixe"): 36 | self.add_error(field, 'Wikipage not found: https://metalab.at/wiki/%s' % wikipage) #TODO Figure out how to make clickable 37 | 38 | def clean(self): 39 | cleaned_data = super().clean() 40 | 41 | start_date = cleaned_data.get('startDate') 42 | end_date = cleaned_data.get('endDate') 43 | if end_date and end_date < start_date: 44 | self.add_error('endDate', 'End date must be greater than start date') 45 | 46 | loc = cleaned_data.get('location') 47 | if loc and loc.name not in ('any rooom', 'online', 'Woanders'): # 48 | if Event.objects.exclude(id=self.instance.id).filter(deleted=False, location__name=loc.name, startDate__lt=end_date, endDate__gt=start_date).count() != 0: 49 | self.add_error('location', 'This location is already in use during the selected time') 50 | 51 | self.clean_wiki_url_fields(cleaned_data, "wikiPage") 52 | self.clean_wiki_url_fields(cleaned_data, "wikiImagePage") 53 | 54 | return cleaned_data 55 | -------------------------------------------------------------------------------- /members/management/commands/generate_many_members.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from dateutil.relativedelta import relativedelta 4 | from django.core.management.base import BaseCommand 5 | from django.db import transaction 6 | 7 | from ...models import ContactInfo 8 | from ...models import KindOfMembership 9 | from ...models import MembershipPeriod 10 | from ...models import Payment 11 | from ...models import PaymentMethod 12 | from ...models import User 13 | 14 | 15 | class Command(BaseCommand): 16 | help = 'Generate a lot of members for testing' 17 | args = "./manage.py generate_many_members absolute_filepath date(yyyy-mm-dd)" 18 | 19 | 20 | def add_arguments(self, parser): 21 | parser.add_argument('--number-of-members', type=int, required=True) 22 | parser.add_argument('--memberships-per-member', type=int, required=True) 23 | 24 | @transaction.atomic 25 | def handle(self, number_of_members, memberships_per_member, *args, **options): 26 | for member_num in range(number_of_members): 27 | user = User.objects.filter(username=f"member{member_num}").delete() 28 | user = User.objects.create(username=f"member{member_num}") 29 | 30 | info = ContactInfo.objects.create( 31 | user=user, 32 | ) 33 | 34 | start_date = datetime.datetime(1960, 1, 1) 35 | 36 | kind_of_membership = KindOfMembership.objects.first() 37 | fee = kind_of_membership.membershipfee_set.first() 38 | fee.start = start_date 39 | fee.save() 40 | 41 | for _ in range(memberships_per_member): 42 | new_start_date = start_date + relativedelta(months=1) 43 | MembershipPeriod.objects.create( 44 | begin=start_date, 45 | end=new_start_date, 46 | user=user, 47 | kind_of_membership=kind_of_membership, 48 | ) 49 | Payment.objects.create( 50 | date=start_date + relativedelta(days=3), 51 | user=user, 52 | amount=10, 53 | method=PaymentMethod.objects.first(), 54 | ) 55 | start_date = new_start_date 56 | 57 | MembershipPeriod.objects.create( 58 | begin=new_start_date, 59 | user=user, 60 | kind_of_membership=kind_of_membership, 61 | ) 62 | -------------------------------------------------------------------------------- /sources/management/commands/jour_fixe_reminder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import requests 4 | from bs4 import BeautifulSoup 5 | from django.conf import settings 6 | from django.core.exceptions import ObjectDoesNotExist 7 | from django.core.mail import send_mail 8 | from django.core.management.base import BaseCommand 9 | from django.template.loader import get_template 10 | 11 | from cal.models import Event 12 | 13 | 14 | def get_next_jf(): 15 | when = datetime.date.today() + datetime.timedelta(days=settings.MOS_JF_DAYS_IN_ADVANCE) 16 | try: 17 | return Event.objects.filter(category_id = settings.MOS_JF_DB_ID, startDate__date=when).first() 18 | except ObjectDoesNotExist: 19 | return None 20 | 21 | def get_wiki_headlines(article): 22 | response = requests.get(settings.HOS_WIKI_FULL_URL + article) 23 | 24 | if not response.ok: 25 | return {"article_missing": True, "error": True, "headlines": []} 26 | 27 | article = BeautifulSoup(response.content, 'html.parser') 28 | in_themen_heading = False 29 | results = [] 30 | 31 | for heading in article.select("h1,h2"): 32 | try: 33 | headline = heading.select(".mw-headline")[0].get_text() 34 | except IndexError: 35 | continue 36 | 37 | if heading.name == "h1": 38 | if in_themen_heading: 39 | break 40 | if headline == "Themen": 41 | in_themen_heading = True 42 | continue 43 | elif in_themen_heading and not headline.startswith("Thema1") and not headline.startswith("Thema2"): 44 | results.append(headline) 45 | 46 | return {"article_missing": False, "error": len(results) == 0, "headlines": results} 47 | 48 | def mail(template, ctx_vars): 49 | tpl = get_template(template) 50 | body = tpl.render(ctx_vars) 51 | subject = get_template(template + ".subject").render(ctx_vars).strip() 52 | send_mail(subject, body, 53 | settings.MOS_JF_SENDER, 54 | settings.MOS_JF_RECIPIENTS, 55 | fail_silently=False) 56 | 57 | class Command(BaseCommand): 58 | help = 'Send the Jour Fixe reminder email, if a Jour Fixe is in settings.MOS_JF_DAYS_IN_ADVANCE days' 59 | 60 | def handle(self, *args, **kwargs): 61 | next_jf = get_next_jf() 62 | 63 | if next_jf: 64 | ctx = {'jf': next_jf, 'wiki': get_wiki_headlines(next_jf.wikiPage)} 65 | mail("jour_fixe_reminder.mail", ctx) 66 | -------------------------------------------------------------------------------- /core/fixtures/default_choices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "core.category", 5 | "fields": { 6 | "name": "metaday", 7 | "description": "die Monatliche Veranstaltung" 8 | } 9 | }, 10 | { 11 | "pk": 2, 12 | "model": "core.category", 13 | "fields": { 14 | "name": "jour fixe", 15 | "description": "wir arbeiten daran" 16 | } 17 | }, 18 | { 19 | "pk": 1, 20 | "model": "core.location", 21 | "fields": { 22 | "country": "austria", 23 | "name": "Hauptraum", 24 | "description": "Hauptraum" 25 | } 26 | }, 27 | { 28 | "pk": 2, 29 | "model": "core.location", 30 | "fields": { 31 | "country": "austria", 32 | "name": "Bibliothek", 33 | "description": "Bibliothek" 34 | } 35 | }, 36 | { 37 | "pk": 5, 38 | "model": "core.location", 39 | "fields": { 40 | "country": "austria", 41 | "name": "*.*", 42 | "description": "*.*" 43 | } 44 | }, 45 | { 46 | "pk": 6, 47 | "model": "core.location", 48 | "fields": { 49 | "country": "?", 50 | "name": "Woanders", 51 | "description": "Woanders" 52 | } 53 | }, 54 | { 55 | "pk": 7, 56 | "model": "core.location", 57 | "fields": { 58 | "country": "austria", 59 | "name": "Lounge (Raucherbetrieb)", 60 | "description": "Lounge (SMOKERS MODE)" 61 | } 62 | }, 63 | { 64 | "pk": 8, 65 | "model": "core.location", 66 | "fields": { 67 | "country": "", 68 | "name": "Whateverlab", 69 | "description": "" 70 | } 71 | }, 72 | { 73 | "pk": 9, 74 | "model": "core.location", 75 | "fields": { 76 | "country": "AT", 77 | "name": "Kueche", 78 | "description": "wo gekocht wird" 79 | } 80 | }, 81 | { 82 | "pk": 11, 83 | "model": "core.location", 84 | "fields": { 85 | "country": "", 86 | "name": "Fotolab", 87 | "description": "" 88 | } 89 | }, 90 | { 91 | "pk": 12, 92 | "model": "core.location", 93 | "fields": { 94 | "country": "", 95 | "name": "any room", 96 | "description": "" 97 | } 98 | }, 99 | { 100 | "pk": 13, 101 | "model": "core.location", 102 | "fields": { 103 | "country": "", 104 | "name": "heavy machinery", 105 | "description": "" 106 | } 107 | }, 108 | { 109 | "pk": 14, 110 | "model": "core.location", 111 | "fields": { 112 | "country": "austria", 113 | "name": "Lounge (Nichtraucherbetrieb)", 114 | "description": "Lounge NON-SMOKERMODE" 115 | } 116 | } 117 | ] 118 | -------------------------------------------------------------------------------- /mos/settings/deploy_env.py: -------------------------------------------------------------------------------- 1 | # Django settings for a deployed instance of MOS 2 | import os 3 | 4 | from .common import * # NOQA 5 | 6 | DEBUG = os.environ.get('DJANGO_DEBUG', 'False').lower() in ('true', 'yes', '1') 7 | 8 | ADMINS = ( 9 | # ('MOS Admin', 'mos@metalab.at'), 10 | ) 11 | 12 | MANAGERS = ADMINS 13 | 14 | USE_X_FORWARDED_HOST = True 15 | ALLOWED_HOSTS = [ os.environ.get('DJANGO_DOMAIN', 'localhost') ] 16 | SESSION_COOKIE_DOMAIN = os.environ.get('DJANGO_DOMAIN', 'localhost') 17 | 18 | # Enable this if you are running behind a reverse proxy. 19 | # You MUST configure the proxy to strip X-Forwarded-Proto to avoid security 20 | # issues! This is how you do it in Apache (enable mod_headers): 21 | # 22 | # RequestHeader unset X-Forwarded-Proto 23 | # RequestHeader set X-Forwarded-Proto https env=HTTPS 24 | # 25 | #SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 26 | 27 | DATABASES = { 28 | 'default': { 29 | 'ENGINE': 'django.db.backends.mysql', 30 | 'NAME': os.environ.get('MYSQL_DATABASE', 'mos'), 31 | 'USER': os.environ.get('MYSQL_USER', 'mos'), 32 | 'PASSWORD': os.environ.get('MYSQL_PASSWORD', 'mos'), 33 | 'HOST': os.environ.get('MYSQL_HOST', 'db'), 34 | 'OPTIONS': { 35 | 'charset': 'utf8mb4', 36 | 'use_unicode': True, 37 | }, 38 | } 39 | } 40 | 41 | SECRET_KEY=os.environ.get('DJANGO_SECRET_KEY') 42 | 43 | STATIC_ROOT = os.environ.get('DJANGO_STATIC_ROOT', 'static') 44 | MEDIA_ROOT = os.environ.get('DJANGO_MEDIA_ROOT', 'media') 45 | 46 | EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST', 'localhost') 47 | EMAIL_PORT = os.environ.get('DJANGO_EMAIL_PORT', 25) 48 | EMAIL_USE_TLS = os.environ.get('DJANGO_EMAIL_USE_TLS', 'False').lower() in ('true', 'yes', '1') 49 | EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_HOST_USER', '') 50 | EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_HOST_PASSWORD', '') 51 | EMAIL_SUBJECT_PREFIX = os.environ.get('DJANGO_EMAIL_SUBJECT_PREFIX', '[MOS] ') 52 | DEFAULT_FROM_EMAIL = os.environ.get('DJANGO_DEFAULT_FROM_EMAIL', 'webmaster@localhost') 53 | 54 | HOS_ANNOUNCE_LOG = os.environ.get('HOS_ANNOUNCE_LOG', '/announce.log') 55 | 56 | HOS_SEPA_CREDITOR_ID = os.environ.get('HOS_SEPA_CREDITOR_ID', 'AT29HXR00000037632') 57 | HOS_SEPA_CREDITOR_IBAN = os.environ.get('HOS_SEPA_CREDITOR_IBAN', 'AT912011182821260400') 58 | HOS_SEPA_CREDITOR_BIC = os.environ.get('HOS_SEPA_CREDITOR_BIC', 'GIBAATWWXXX') 59 | 60 | MATRIX_PASSWORD = os.environ.get('MATRIX_PASSWORD', None) 61 | MATRIX_ROOM_ID = os.environ.get('MATRIX_ROOM_ID', None) 62 | -------------------------------------------------------------------------------- /templates/members/member_bank_json_match.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block hos_content %} 4 |

    Erste Bank JSON Import

    5 |
     6 | HOWTO
     7 | 
     8 | * unten sind alle eingelesenen Buchungen, Buchungen von Wien Energie und co werden ignoriert
     9 | * Member werden anhand ihrer eingetragenen IBAN vorausgewählt
    10 | * Rückläufer sind rot
    11 | * schon gebuchte Zeilen sind grau und nicht vorausgewählt
    12 | 
    13 | 
    14 |
    15 | {% csrf_token %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% for row in import_rows %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 56 | 57 | {% endfor %} 58 | 59 |
    DatumName/IBANReferenzBetragMember
    {{ row.payment.booking|date }}{{ row.payment.partnerName }}
    {{ row.payment.partnerAccount.iban }}
    {{ row.payment.text }} 38 | {{ row.payment.amount.value_full|floatformat:2 }} 39 | 41 | 55 |
    60 |
    61 | 62 |
    63 |
    64 |
    65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /projects/templates/projects/projectinfo.inc: -------------------------------------------------------------------------------- 1 | {% with form_id=project.id|default_if_none:'' %} 2 |
    3 |

    4 | {% if project.wikiPage %} {% endif %}{{ project.name }}{% if project.wikiPage %}{% endif %} 5 | {% if user.is_authenticated %}{% if project %}Edit{% else %}Create New Project{% endif %}{% endif %}{% if project.teaser %}
    {{ project.teaser }}{% endif %} 6 |

    7 | {% if user.is_authenticated %} 8 |
    9 |
    11 | {% csrf_token %} 12 |
    13 |
    14 | {% if project_form.name.errors %}
    {{ project_form.name.errors }}
    {% endif %} 15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 | 23 |
    24 |
    25 |
    * -> required field
    26 |
    Submit:
    27 |
    28 | {% if project %} 29 | 30 | 31 | {% else %} 32 | 33 | {% endif %} 34 | Cancel 35 |
    36 |
    37 |
    38 |
    39 | {% endif %} 40 |
    41 | {% endwith %} 42 | -------------------------------------------------------------------------------- /members/management/commands/invite_matrix_users.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import redirect_stdout 3 | from io import StringIO 4 | 5 | import matrix_commander 6 | from django.conf import settings 7 | from django.core.management.base import BaseCommand 8 | 9 | from members.models import get_intern_matrix_members 10 | 11 | 12 | def call_matrix_commander(*args): 13 | return matrix_commander.main([ 14 | "matrix-commander", 15 | "-s", "matrix_client_store", 16 | "-c", "matrix_client_store/credentials.json", 17 | *args, 18 | ]) 19 | 20 | 21 | def matrix_login(): 22 | call_matrix_commander( 23 | "--homeserver", "https://matrix.org", 24 | "--user-login", settings.MATRIX_USERNAME, 25 | "--password", settings.MATRIX_PASSWORD, 26 | "--login", "password", 27 | "--device", "device", 28 | "--room-default", settings.MATRIX_ROOM_NAME, 29 | ) 30 | 31 | 32 | def matrix_invite(handles): 33 | for matrix_handle in handles: 34 | call_matrix_commander("--room-invite", settings.MATRIX_ROOM_NAME, "--user", matrix_handle) 35 | 36 | 37 | def matrix_kick(handles): 38 | for matrix_handle in handles: 39 | call_matrix_commander("--room-kick", settings.MATRIX_ROOM_NAME, "--user", matrix_handle) 40 | 41 | 42 | def get_channel_members(): 43 | with redirect_stdout(StringIO()) as buffer: 44 | call_matrix_commander("--joined-members", settings.MATRIX_ROOM_ID) 45 | current_members_output = buffer.getvalue() 46 | current_members_output = [m.strip().partition(" ")[0] for m in current_members_output.splitlines()] 47 | return set( 48 | m 49 | for m in current_members_output 50 | if m.startswith("@") 51 | ) 52 | 53 | 54 | class Command(BaseCommand): 55 | def handle(self, *args, **kwargs): 56 | if not settings.MATRIX_ROOM_ID or not settings.MATRIX_ROOM_NAME or not settings.MATRIX_USERNAME or not settings.MATRIX_PASSWORD: 57 | print("Missing MATRIX_* config, check settings and try again.") 58 | sys.exit(1) 59 | 60 | matrix_login() 61 | 62 | members_should = set(get_intern_matrix_members().values_list("contactinfo__matrix_handle", flat=True)) 63 | members_should.add("@metalab_room_inviter_bot:matrix.org") 64 | members_should.add("@metalab_room_owner_bot:matrix.org") 65 | members_is = get_channel_members() 66 | 67 | members_kick = (members_is - members_should) 68 | members_invite = (members_should - members_is) 69 | 70 | print(f"inviting: {members_invite}") 71 | matrix_invite(members_invite) 72 | print(f"kicking: {members_kick}") 73 | matrix_kick(members_kick) 74 | -------------------------------------------------------------------------------- /sources/tests/test_cronjob.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | 4 | import feedparser 5 | from django.conf import settings 6 | from django.core.management.base import CommandError 7 | from django.test import TestCase 8 | 9 | from sources.management.commands import get_wiki_changes 10 | from sources.models import WikiChange 11 | 12 | 13 | class FakeFeed: 14 | class FakeFeedEntry: 15 | link = '' 16 | author = '' 17 | updated_parsed = time.strptime('2000-01-01', '%Y-%m-%d') 18 | 19 | def __init__(self, title): 20 | self.title = title 21 | 22 | def __init__(self, url): 23 | pass 24 | 25 | def __contains__(self, key): 26 | return False 27 | 28 | def __getattr__(self, attr): 29 | return [self.FakeFeedEntry(c) for c in 'qwert'] 30 | 31 | 32 | class CronJobTest(TestCase): 33 | def setUp(self): 34 | self.cmd = get_wiki_changes.Command() 35 | 36 | dt = datetime.datetime.now() 37 | self.changes = [WikiChange(title=t, updated=dt) for t in 'asdfg'] 38 | WikiChange.objects.bulk_create(self.changes) 39 | 40 | def test_raises_exception_on_parse_error(self): 41 | real_url = settings.MOS_WIKI_CHANGE_URL 42 | settings.MOS_WIKI_CHANGE_URL = 'xxx' 43 | 44 | self.assertRaises(CommandError, self.cmd.handle) 45 | 46 | settings.MOS_WIKI_CHANGE_URL = real_url 47 | 48 | def test_no_db_changes_on_error(self): 49 | real_url = settings.MOS_WIKI_CHANGE_URL 50 | settings.MOS_WIKI_CHANGE_URL = 'xxx' 51 | 52 | try: 53 | self.cmd.handle() 54 | except: 55 | pass 56 | 57 | self.assertEqual(WikiChange.objects.count(), 5) 58 | for c in 'asdfg': 59 | WikiChange.objects.get(title=c) # does not raise 60 | 61 | settings.MOS_WIKI_CHANGE_URL = real_url 62 | 63 | def test_old_entries_are_deleted(self): 64 | self.cmd.handle() 65 | for c in 'asdfg': 66 | self.assertRaises(WikiChange.DoesNotExist, 67 | WikiChange.objects.get, title=c) 68 | 69 | def test_five_entries_are_kept(self): 70 | real_parse = feedparser.parse 71 | 72 | try: 73 | feedparser.parse = FakeFeed 74 | self.cmd.handle() 75 | self.assertEqual(WikiChange.objects.count(), 5) 76 | 77 | for c in 'qwert': 78 | WikiChange.objects.get(title=c) # does not raise 79 | for c in 'asdfg': 80 | self.assertRaises( 81 | WikiChange.DoesNotExist, 82 | WikiChange.objects.get, title=c 83 | ) 84 | finally: 85 | feedparser.parse = real_parse 86 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | 2 | ====================================================================== 3 | HOW TO SET UP A LOCAL INSTALLATION OF METALAB OS TO CONTRIBUTE PATCHES 4 | ====================================================================== 5 | 6 | 1. git clone git@github.com:Metalab/mos.git && cd mos 7 | 8 | 2. On a Debian-based system: 9 | apt-get install python3-venv python3-dev default-libmysqlclient-dev build-essential pkg-config 10 | 11 | On OpenSuSE: 12 | zypper install python-venv pkg-config 13 | 14 | On any other system: 15 | easy_install pip 16 | pip install venv 17 | 18 | 3. Create the virtualenv (call it "devel"): python3 -m venv devel 19 | 4. Acivate the "devel" virtualenv: source devel/bin/activate 20 | 5. Install dependencies: pip install -r requirements.txt 21 | (Note: Requires a C compiler and the python development headers. 22 | Debian: apt-get install python-dev build-essential 23 | OpenSuSE: zypper in -t pattern devel_C_C++ devel_python) 24 | 25 | 6. Install dev dependencies: pip install -r requirements-dev.txt 26 | 6b. Install pre-commit to ensure linting and code style: pre-commit install --install-hooks 27 | 7. Optional: Install the locale packages for de_DE.UTF-8, like so https://unix.stackexchange.com/a/669800 28 | 8. Put a temporary key into mos/settings/secret_key.py (SECRET_KEY='bla') 29 | 8a. python3 manage.py generate_secret_key (optional for development) and then 30 | 8b. Put output into secret_key.py (SECRET_KEY='') (also optional) 31 | 9. python3 manage.py migrate 32 | 10. python3 manage.py createsuperuser 33 | 34 | 11. Load example data (optional, but recommended): 35 | python3 manage.py loaddata core/fixtures/default_choices.json 36 | python3 manage.py loaddata members/fixtures/default_choices.json 37 | python3 manage.py loaddata members/fixtures/dummy_members.json 38 | python3 manage.py loaddata cal/fixtures/events_2012-09-20.json 39 | 40 | 12. python3 manage.py runserver 41 | 13. Point your browser to http://127.0.0.1:8000/ 42 | 14. Login with your freshly-created user account 43 | 44 | Testing 45 | ======= 46 | After you've made some changes to the code, rerun the test suite to check 47 | that everything still works. You can do this from the project root by issuing: 48 | 49 | ./manage.py test 50 | 51 | If you have a test failing, you can rerun only the app responsible to iterate 52 | faster. E.g. if you have an error somewhere inside the cal package, do: 53 | 54 | ./manage.py test cal 55 | 56 | 57 | Further Reading 58 | =============== 59 | 60 | Virtualenv/Pip Basics: https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/ 61 | Python Docs: https://docs.python.org/ 62 | Django Docs: https://docs.djangoproject.com/ 63 | -------------------------------------------------------------------------------- /templates/cal/eventinfo_detail.inc: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 | 7 | {% if event.past %} 8 |
    Achtung: Event liegt bereits in der Vergangenheit!
    9 | {% endif %} 10 | 11 |

    Event: {{ event.name }}

    12 |

    13 | {% if event.teaser %}Teaser: {{ event.teaser }}{% endif %} 14 |

    15 | {% if event.category %} 16 |

    17 | Kategorie: {{ event.category }} 18 |

    19 | {% endif %} 20 |

    21 | 22 | {{ event.startDate|date:"D d.m.Y H:i" }} {% if event.endDate %} - {% if event.start_end_date_eq %} {{ event.endDate|date:"H:i" }} {% else %} {{ event.endDate|date:"D d.m.Y H:i" }} {% endif %} {% endif %} 23 | 24 |

    25 | {% if event.location%} 26 |

    27 | Ort: {{ event.location }} 28 |

    29 | {% endif %} 30 | {% if event.wikiPage %} 31 |

    32 | Details: {{ request.get_host }}/wiki/{{event.wikiPage}} 33 |

    34 | {% endif %} 35 | 36 | {% if user.is_authenticated %} 37 |

    38 | {% if event.advertise %} 39 | Dieser Event kann öffentlich beworben werden. 40 | {% else %} 41 | Dieser Event soll nicht öffentlich beworben werden. 42 | {% endif %} 43 |

    44 |

    45 | ical: {{ request.get_host }}{{event.get_icalendar_url}} 46 |

    47 |

    48 | Das Event wurde am {{ event.created_at|date:"d.m.Y" }} um {{ event.created_at|date:"H:i" }} von {{ event.created_by }} erstellt. 49 |

    50 | 51 | {% if not edit_disabled %} 52 |

    53 | 54 | Edit 55 | 56 | 57 | 58 | Delete 59 | 60 | 61 |

    62 | {% endif %} 63 | 64 | {% endif %} 65 | 66 |
    67 | 68 | {% if user.is_authenticated %} 69 | {% include "cal/event_form.inc" with from_nf=0 %} 70 | {% endif %} 71 |
    72 |
    73 | -------------------------------------------------------------------------------- /web/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import JsonResponse 3 | from django.shortcuts import render 4 | 5 | from cal.models import Event 6 | from core.context_processors import custom_settings_main 7 | from members.models import get_active_members 8 | from projects.models import Project 9 | from sources.models import WikiChange 10 | 11 | 12 | def display_main_page(request): 13 | events = Event.future.get_n(5) 14 | changes = WikiChange.objects.order_by('-updated')[:5] 15 | projects = Project.all.order_by('-created_at')[:5] 16 | randommembers = list(get_active_members().exclude(contactinfo__image="").order_by('?')[:7]) 17 | 18 | context = custom_settings_main(request) 19 | context.update( 20 | {'event_error_id': ' ', 21 | 'latestevents': events, 22 | 'latestchanges': changes, 23 | 'latestprojects': projects, 24 | 'randommembers': randommembers} 25 | ) 26 | return render(request, 'index.html', context) 27 | 28 | 29 | def display_cellardoor(request): 30 | context = custom_settings_main(request) 31 | context['latestevents'] = Event.future.all() 32 | return render(request, 'cellardoor.html', context) 33 | 34 | 35 | def spaceapi(request): 36 | # See http://spaceapi.net/documentation 37 | 38 | projects = Project.all.order_by('-created_at')[:5] 39 | 40 | return JsonResponse({ 41 | 'api': '0.13', 42 | 'space': 'Metalab', 43 | 'logo': 'https://metalab.at/static/images/logo.png', 44 | 'url': 'https://metalab.at/', 45 | 'location': { 46 | # https://metalab.at/wiki/Lage 47 | 'address': u'Rathausstra\xdfe 6, 1010 Vienna, Austria', 48 | 'lat': 48.2093723, 49 | 'lon': 16.356099, 50 | }, 51 | 'contact': { 52 | 'twitter': '@metalab_events', 53 | 'irc': 'irc://irc.libera.chat/#metalab', 54 | 'email': 'core@metalab.at', 55 | 'ml': 'metalab@lists.metalab.at', 56 | 'jabber': 'metalab@conference.jabber.metalab.at', 57 | 'phone': '+43 720 00 23 23', 58 | }, 59 | 'issue_report_channels': [ 60 | 'email', 61 | ], 62 | 'feeds': { 63 | 'wiki': { 64 | 'type': 'atom', 65 | 'url': settings.MOS_WIKI_CHANGE_URL, 66 | }, 67 | 'calendar': { 68 | 'type': 'rss', 69 | 'url': 'https://metalab.at/feeds/events/', 70 | }, 71 | #'blog': { 72 | # 'type': 'rss', 73 | # 'url': 'http://metalab.soup.io/rss', 74 | #}, 75 | }, 76 | 'projects': ['https://metalab.at/wiki/%s' % project.wikiPage for project in projects if project.wikiPage], 77 | 'state': { 78 | # TODO: Implement open state tracking 79 | 'open': None, 80 | }, 81 | }) 82 | -------------------------------------------------------------------------------- /members/util.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from datetime import timedelta 3 | 4 | from dateutil.relativedelta import relativedelta 5 | from dateutil.rrule import MONTHLY 6 | from dateutil.rrule import rrule 7 | from django.contrib.auth.models import User 8 | 9 | from .models import MembershipPeriod 10 | 11 | 12 | def get_date_of_entry(user): 13 | mp_list = MembershipPeriod.objects.filter(user__exact=user).order_by('begin')[:1] 14 | if len(mp_list) < 1: 15 | return None 16 | return mp_list[0].begin 17 | 18 | 19 | def get_date_of_exit(user): 20 | mp_list = MembershipPeriod.objects.filter(user__exact=user).order_by('-begin')[:1] 21 | if len(mp_list) < 1: 22 | return None 23 | return mp_list[0].end 24 | 25 | 26 | class HistoryEntry: 27 | month = date.today() 28 | num_member = 0 29 | new_member = 0 30 | resigned_member = 0 31 | 32 | 33 | def get_list_of_history_entries(): 34 | end_of_month = date.today() + relativedelta(day=31) 35 | 36 | he_list = {} 37 | month_list = rrule(MONTHLY, dtstart=date(2006, 3, 1), until=end_of_month) 38 | for month in month_list: 39 | d = date(month.year, month.month, month.day) 40 | he_list[d] = HistoryEntry() 41 | he_list[d].month = d 42 | 43 | for u in User.objects.all(): 44 | mps = u.membershipperiod_set.values_list('begin', 'end') 45 | if not mps: 46 | continue 47 | 48 | starts, ends = zip(*mps) 49 | starts = list(starts) + [None] 50 | ends = [None] + list(ends) 51 | 52 | for end, start in zip(ends, starts): 53 | if end is None and start is None: 54 | continue 55 | 56 | if end is None: 57 | he_list[start.replace(day=1)].new_member += 1 58 | continue 59 | 60 | if start is None: 61 | if end <= end_of_month: 62 | he_list[end.replace(day=1)].resigned_member += 1 63 | continue 64 | 65 | pause = start.replace(day=1) - (end + relativedelta(day=31)) 66 | if pause > timedelta(1): 67 | he_list[start.replace(day=1)].new_member += 1 68 | if end <= end_of_month: 69 | he_list[end.replace(day=1)].resigned_member += 1 70 | 71 | num = 0 72 | for month in month_list: 73 | he = he_list[month.date()] 74 | num += he.new_member 75 | num -= he.resigned_member 76 | he.num_member = num 77 | 78 | return he_list 79 | 80 | 81 | def generate_bank_collection_list(for_month): 82 | member_list = Member.objects.all() 83 | for_month = date(for_month.year, for_month.month, 1) 84 | list = {} 85 | for member in member_list: 86 | if member.bank_collection_allowed: 87 | fee = member.get_membership_fee(for_month) 88 | if fee is not None and fee.amount > 0: 89 | list[member] = fee.amount 90 | 91 | return list 92 | -------------------------------------------------------------------------------- /templates/cellardoor.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | You are not my homebase dude.. 8 | 9 | 10 | 11 | 72 | 73 | 74 | 75 |
    76 |
    77 |
    78 | 79 |
    80 |
    81 |
    Was passiert denn hier?
    82 |
    Was passiert denn hier?
    83 |
    84 | 85 |
      86 | {% include "cal/calendar.inc" %} 87 |
    88 |
    89 |
    init
    90 | 91 |
    92 |
    93 |
    94 | 95 | 96 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}metalab{% endblock %} 4 | 5 | {% block hos_content %} 6 |
    7 |
    8 |

    Events

    9 | {% include "cal/calendar.inc" %} 10 |
    11 |
    12 | Interessiert? Dann schau doch am besten vorbei oder join unseren Matrix Raum bzw. eine der Mailinglisten. 13 |
    14 |
    15 |
    16 | {% comment %} 17 | 18 |
    19 | 20 | 21 | 22 | {% endcomment %} 23 |

    Mitglieder

    24 |
    25 | {% for member in randommembers %} 26 | {% if member.contactinfo.get_wikilink %}{% endif %}{{ member.username }}{% if member.contactinfo.get_wikilink %}{% endif %} 27 | {% endfor %} 28 | Mitglied werden 29 |
    30 |
    31 | 220m² Raum im Herzen Wiens für technologisch-kreative Projekte, Veranstaltungen, Software, Hardware, Essen & mehr.... 32 |
    33 | 35 | 44 |
    45 | Willst du das Metalab besuchen? Komm Abends vorbei oder besuche uns am Open Day (Kalender links)!
    46 |
    47 | Want to visit the Metalab? Come by in the evening or visit us on Open Day (calendar on the left)!
    48 |
    49 |
    50 |

    Projekte

    51 | {% include "projects/overview.inc" %} 52 |
    53 | 54 |
    55 | {% include "rss/recentchanges.inc" %} 56 |
    57 | 58 |
    59 |

    Issue Tracker

    60 |
    61 | 62 | 65 |
    66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | $msg = <\d{4})/$', 45 | YearArchiveView.as_view( 46 | queryset=Event.all.all(), 47 | date_field="startDate", 48 | allow_future=True, 49 | make_object_list=True, 50 | ), 51 | {}, 52 | "cal_archive_year", 53 | ), 54 | re_path( 55 | r'^(?P\d{4})/(?P\d{2})/$', 56 | cal.views.monthly, 57 | ), 58 | re_path( 59 | r'^special/(?P\w+)/(?P[\w ]+)/$', 60 | cal.views.display_special_events, 61 | ), 62 | re_path( 63 | r'^event/(?P\d+)/$', 64 | DetailView.as_view( 65 | queryset=Event.all.all(), 66 | context_object_name='event', 67 | ), 68 | {}, 69 | 'cal_event_detail', 70 | ), 71 | re_path( 72 | r'^event/(?P\d+)/update/$', 73 | cal.views.update_event, 74 | { 'new': False }, 75 | ), 76 | re_path( 77 | r'^(?P\d+)/update/$', 78 | cal.views.update_event, 79 | { 'new': False }, 80 | ), 81 | re_path( 82 | r'^event/(?P\d+)/delete/', 83 | cal.views.delete_event, 84 | ), 85 | re_path( 86 | r'^(?P\d+)/delete/', 87 | cal.views.delete_event, 88 | ), 89 | re_path( 90 | r'^event/(?P\d+)/icalendar/', 91 | cal.views.event_icalendar, 92 | {}, 93 | 'cal_event_icalendar', 94 | ), 95 | path( 96 | 'export/ical/', 97 | partial(cal.views.complete_ical, num=100, past_duration=datetime.timedelta(days=7)), 98 | {}, 99 | 'small_ical', 100 | ), 101 | path( 102 | 'export/ical_full/', 103 | partial(cal.views.complete_ical, num=0, past_duration=None), 104 | {}, 105 | 'full_ical', 106 | ), 107 | path( 108 | 'event/new/', 109 | cal.views.update_event, 110 | { 'new': True }, 111 | ), 112 | path( 113 | 'new/', 114 | cal.views.update_event, 115 | { 'new': True }, 116 | ), 117 | re_path( 118 | r'^ajax/list/(?P\d*)/?$', 119 | cal.views.event_list, 120 | ), 121 | re_path( 122 | r'^api/public_upcoming/?$', 123 | cal.views.public_upcoming, 124 | ), 125 | ] 126 | -------------------------------------------------------------------------------- /announce/views.py: -------------------------------------------------------------------------------- 1 | # 2 | # Views for issueing announcements to all active members. 3 | # 4 | from datetime import date 5 | from datetime import datetime 6 | from functools import partial 7 | 8 | import django.forms as forms 9 | from django.conf import settings 10 | from django.contrib.admin.views.decorators import staff_member_required 11 | from django.db.models import Q 12 | from django.shortcuts import render 13 | 14 | from members.models import KindOfMembership 15 | from members.models import get_active_members 16 | from members.models import members_due_for_bank_collection 17 | 18 | 19 | def _announce_filter_collection(users): 20 | users = members_due_for_bank_collection(users) 21 | for u in users: 22 | debt = u.contactinfo.get_debt_for_month(date.today()) 23 | if debt == 0: 24 | users = users.exclude(pk=u.pk) 25 | return users 26 | 27 | def _announce_filter_keymembers(users): 28 | return users.filter(contactinfo__has_active_key=True) \ 29 | .exclude(contactinfo__key_id=None) 30 | 31 | 32 | def _announce_filter_fee_category_members(users, fee_category): 33 | return users.filter( 34 | Q(membershipperiod__begin__lt=datetime.now()) & 35 | (Q(membershipperiod__end__isnull=True) | Q(membershipperiod__end__gt=datetime.now())) & 36 | Q(membershipperiod__kind_of_membership__fee_category=fee_category) 37 | ) 38 | 39 | 40 | ANNOUNCE_TARGETS = { 41 | 'collection': ('collection', _announce_filter_collection), 42 | 'keymembers': ('keymembers', _announce_filter_keymembers), 43 | **{ 44 | cat[0]: (cat[1] + ' members', partial(_announce_filter_fee_category_members, fee_category=cat[0])) 45 | for cat in KindOfMembership.FEE_CATEGORY 46 | }, 47 | 'all': ('all', lambda users: users), 48 | } 49 | 50 | class AnnouncementForm(forms.Form): 51 | subject = forms.CharField(required=True, label="Thema", max_length=120) 52 | body = forms.CharField(required=True, label="Mitteilung", 53 | widget=forms.Textarea) 54 | to = forms.ChoiceField(required=True, label="An", choices=((k, v[0]) for k, v in ANNOUNCE_TARGETS.items())) 55 | 56 | 57 | @staff_member_required 58 | def announce(request): 59 | form = AnnouncementForm(request.POST or None) 60 | if not request.POST or not form.is_valid(): 61 | context = {'form': form, 'user': request.user} 62 | return render(request, 'announce/write_message.html', context) 63 | # Valid message: send it! 64 | users = get_active_members() 65 | 66 | users = ANNOUNCE_TARGETS[form.cleaned_data['to']][1](users) 67 | 68 | for user in users: 69 | body = form.cleaned_data['body'] \ 70 | .replace('{{username}}', user.get_username()) \ 71 | .replace('{{full_name}}', user.get_full_name()) \ 72 | .replace('{{short_name}}', user.get_short_name()) \ 73 | .replace('{{first_name}}', user.first_name) \ 74 | .replace('{{last_name}}', user.last_name) \ 75 | .replace('{{user_id}}', str(user.pk)) \ 76 | .replace('{{profile_link}}', 77 | f'https://{settings.SESSION_COOKIE_DOMAIN}/member/{user.get_username()}/') \ 78 | .replace('{{IBAN}}', str(settings.HOS_SEPA_CREDITOR_IBAN)) \ 79 | .replace('{{BIC}}', str(settings.HOS_SEPA_CREDITOR_BIC)) 80 | 81 | user.contactinfo.send_mail(form.cleaned_data['subject'], body, settings.HOS_ANNOUNCE_LOG) 82 | 83 | context = {'form': form, 'user': request.user, 'users': users} 84 | return render(request, 'announce/message_sent.html', context) 85 | -------------------------------------------------------------------------------- /templates/cal/event_form.inc: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {% csrf_token %} 4 |
    5 | 6 |
    7 |
    8 | {{ event_form.name }} -
    9 | {{ event_form.teaser }} 10 |
    11 | {% if event_form.name.errors %} 12 |
    {{ event_form.name.errors }}
    13 | {% endif %} 14 | 15 |
    16 |
    {{ event_form.wikiPage }}
    17 | {% if event_form.wikiPage.errors %} 18 |
    {{ event_form.wikiPage.errors }}
    19 | {% endif %} 20 | 21 |
    22 |
    {{ event_form.wikiImagePage }}
    23 | {% if event_form.wikiImagePage.errors %} 24 |
    {{ event_form.wikiImagePage.errors }}
    25 | {% endif %} 26 | 27 |
    28 |
    {{ event_form.startDate }}
    29 | {% if event_form.startDate.errors %} 30 |
    {{ event_form.startDate.errors }}
    31 | {% endif %} 32 | 33 |
    34 | 35 |
    {{ event_form.endDate }}
    36 | {% if event_form.endDate.errors %} 37 |
    {{ event_form.endDate.errors }}
    38 | {% endif %} 39 | 40 |
    41 |
    {{ event_form.location }}
    42 | {% if event_form.location.errors %} 43 |
    {{ event_form.location.errors }}
    44 | {% endif %} 45 | 46 |
    47 |
    {{ event_form.category }}
    48 | {% if event_form.category.errors %} 49 |
    {{ event_errors.category }}
    50 | {% endif %} 51 | 52 |
    53 |
    {{ event_form.advertise }}
    54 | {% if event_form.advertise.errors %} 55 |
    {{ event_errors.advertise }}
    56 | {% endif %} 57 | 58 | (*) -> required field 59 | 60 | 61 |
    Submit:
    62 |
    63 |
    64 | {% if not new %} 65 | 66 | {% if from_nf %} 67 | 68 | {% endif %} 69 | {% else %} 70 | 71 | {% endif %} 72 | {% if from_nf %} 73 | Cancel 74 | {% endif %} 75 |
    76 |
    77 |
    78 |
    79 | -------------------------------------------------------------------------------- /provision_vagrant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | vars: 4 | virtualenv_path: /home/vagrant/dev 5 | tasks: 6 | - name: link /mos -> /vagrant 7 | file: 8 | src: /vagrant 9 | dest: /mos 10 | state: link 11 | 12 | # update / install packages 13 | - name: "Install packages" 14 | become: true 15 | apt: 16 | update_cache: true 17 | cache_valid_time: 3600 18 | pkg: 19 | - build-essential 20 | - locales 21 | - python3-dev 22 | - python3-pip 23 | - python3-venv 24 | # - rustc 25 | 26 | - name: "add locale de_DE.UTF-8" 27 | become: true 28 | locale_gen: 29 | name: "de_DE.UTF-8" 30 | state: present 31 | 32 | - name: Upgrade pip3 33 | pip: 34 | name: 35 | - pip 36 | - wheel 37 | extra_args: --upgrade 38 | virtualenv: "{{ virtualenv_path }}" 39 | virtualenv_command: /usr/bin/python3 -m venv 40 | 41 | # install required python modules 42 | - name: Install python modules 43 | pip: 44 | requirements: "{{ item }}" 45 | virtualenv: "{{ virtualenv_path }}" 46 | virtualenv_command: /usr/bin/python3 -m venv 47 | with_items: 48 | - /vagrant/requirements.txt 49 | - /vagrant/requirements-dev.txt 50 | 51 | # a generated file should contain something like: SECRET_KEY=' /mos/mos/settings/secret_key.py 58 | when: not st.stat.exists 59 | 60 | # prepare MOS itself 61 | - name: generate mos sqlite database 62 | stat: path=/mos/mos.sqlite 63 | register: st_sqlite 64 | 65 | - name: django makemigrations 66 | django_manage: 67 | app_path: /mos 68 | command: makemigrations 69 | virtualenv: "{{ virtualenv_path }}" 70 | 71 | - name: django migrate 72 | django_manage: 73 | app_path: /mos 74 | command: migrate 75 | virtualenv: "{{ virtualenv_path }}" 76 | 77 | - name: Create Admin User 78 | django_manage: 79 | app_path: /mos 80 | command: "createsuperuser --noinput --username=admin --email=admin@example.com" 81 | virtualenv: "{{ virtualenv_path }}" 82 | when: not st_sqlite.stat.exists 83 | 84 | - name: django loaddata 85 | django_manage: 86 | app_path: /mos 87 | fixtures: 'core/fixtures/default_choices.json members/fixtures/default_choices.json members/fixtures/dummy_members.json cal/fixtures/events_2012-09-20.json' 88 | command: loaddata 89 | virtualenv: "{{ virtualenv_path }}" 90 | when: not st_sqlite.stat.exists 91 | 92 | # If calender data fixture fails because of auth.user 1 not there. Creating Admin failed... 93 | # Delete mos.sqlite DB in folder and provision vagrant Box again. 94 | 95 | - name: Activate virtualenv on vagrant ssh 96 | lineinfile: 97 | dest: /home/vagrant/.bashrc 98 | line: 'source {{ virtualenv_path }}/bin/activate' 99 | 100 | # set the default directory to /mos 101 | - name: Change into mos dir on vagrant ssh 102 | lineinfile: 103 | dest: /home/vagrant/.bashrc 104 | line: 'cd /mos' 105 | -------------------------------------------------------------------------------- /static/stylesheets/global.css: -------------------------------------------------------------------------------- 1 | /* ------------------------- 2 | Metalab CSS 3 | 4 | Version 1.0 5 | ---------------------------*/ 6 | 7 | html { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | font: 76% "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif; 13 | color: #444; 14 | line-height: 1.3em; 15 | } 16 | 17 | /* text formating */ 18 | h1, 19 | h2, 20 | h3 { 21 | line-height: 1em; 22 | } 23 | 24 | h1 { 25 | font-size: 1.5em; 26 | color: #4f5c7f; 27 | margin: 0 0 1em 0; 28 | } 29 | 30 | h2 { 31 | font-size: 1.1em; 32 | color: #4f5c7f; 33 | margin: 1.5em 0 1em 0; 34 | } 35 | 36 | h3 { 37 | font-size: 1em; 38 | font-weight: bold; 39 | color: #444; 40 | margin: 1.5em 0 1em 0; 41 | } 42 | 43 | p { 44 | margin: 1em 0; 45 | } 46 | 47 | a, 48 | #logout-form button { 49 | text-decoration: none; 50 | color: #5d5ea2; 51 | } 52 | 53 | a:hover, 54 | #logout-form button:hover { 55 | color: #ffffff; 56 | background: #8c9ce9; 57 | } 58 | 59 | img { 60 | border: none; 61 | } 62 | 63 | strong { 64 | font-weight: bold; 65 | } 66 | 67 | small { 68 | font-size: 0.9em; 69 | } 70 | 71 | /* global classes */ 72 | .ir { 73 | text-indent: -100em; 74 | overflow: hidden; 75 | } /* image replacement - hides text from browsers with css on */ 76 | 77 | /* form classes */ 78 | .tf { 79 | border: 1px solid #ccc; 80 | } 81 | 82 | /* table styles */ 83 | td { 84 | background: #fff; 85 | padding: 3px 5px; 86 | vertical-align: top; 87 | } 88 | 89 | .thumb_with_text { 90 | margin-top: 3px; 91 | } 92 | 93 | #table_user_details td { 94 | padding: 3px; 95 | text-align: top; 96 | } 97 | 98 | #table_user_list td { 99 | padding: 10px 2px; 100 | text-align: top; 101 | } 102 | 103 | tr.even td { 104 | background-color: #eee; 105 | } 106 | tr.odd td { 107 | background-color: #fff; 108 | } 109 | 110 | /* Forms */ 111 | input[type="email"], 112 | input[type="number"], 113 | input[type="password"], 114 | input[type="tel"], 115 | input[type="url"], 116 | input[type="text"] { 117 | border: 1px solid #8c9ce9; 118 | padding: 0.3em 0.5em; 119 | margin: 0.2em 0; 120 | font-family: inherit; 121 | font-size: 14px; 122 | line-height: 1em; 123 | box-sizing: border-box; 124 | } 125 | 126 | dd { 127 | margin-bottom: 0.6em; 128 | } 129 | dt { 130 | margin-bottom: 0.2em; 131 | } 132 | 133 | .btn { 134 | background-color: #4f5c7f; 135 | color: #fff; 136 | font-weight: bold; 137 | display: inline-block; 138 | font-size: 1.2em; 139 | line-height: 1.6em; 140 | text-align: center; 141 | padding: 0.3em 0.6em; 142 | min-width: 1.2em; 143 | border: none; 144 | cursor: pointer; 145 | } 146 | .btn:hover { 147 | background: #8c9ce9; 148 | } 149 | 150 | /* style logout form as link */ 151 | #logout-form { 152 | display: inline; 153 | } 154 | 155 | #logout-form button { 156 | background: none; 157 | border: none; 158 | cursor: pointer; 159 | padding: 0; 160 | } 161 | 162 | /* list items inline */ 163 | .inline-list { 164 | display: block; 165 | } 166 | .inline-list > li { 167 | display: inline; 168 | } 169 | 170 | /* notifications */ 171 | .notification { 172 | padding: 0.5em; 173 | margin: 0.3em; 174 | border-radius: 5px; 175 | background: #fff; 176 | border: #aaa 3px solid; 177 | } 178 | .notification .event { 179 | margin-bottom: 0; 180 | } 181 | 182 | .notification h3 { 183 | font-size: 1.2em; 184 | margin: 0; 185 | } 186 | .success { 187 | border-color: #00bf00; 188 | } 189 | .warning { 190 | border-color: #dada10; 191 | } 192 | .error { 193 | border-color: #e31515; 194 | } 195 | .info { 196 | border-color: #62a7f5; 197 | } 198 | .messages > li { 199 | border-style: solid; 200 | border-width: 1px; 201 | margin: 0.2rem 0; 202 | padding: 0.2rem; 203 | } 204 | ul.errorlist { 205 | border: none !important; 206 | color: #e31515; 207 | margin: 0 0 10px; 208 | } 209 | 210 | td.number { 211 | text-align: right; 212 | } 213 | -------------------------------------------------------------------------------- /static/javascripts/ml-ajaxtools.js: -------------------------------------------------------------------------------- 1 | Ajax.Base.prototype.initialize = Ajax.Base.prototype.initialize.wrap( 2 | function (callOriginal, options) { 3 | var headers = options.requestHeaders || {}; 4 | headers["X-CSRFToken"] = getCookie("csrftoken"); 5 | options.requestHeaders = headers; 6 | return callOriginal(options); 7 | }); 8 | 9 | function getCookie(name) { 10 | var cookieValue = null; 11 | if (document.cookie && document.cookie != '') { 12 | var cookies = document.cookie.split(';'); 13 | for (var i = 0; i < cookies.length; i++) { 14 | var cookie = cookies[i].trim(); 15 | // Does this cookie string begin with the name we want? 16 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 17 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 18 | break; 19 | } 20 | } 21 | } 22 | return cookieValue; 23 | } 24 | 25 | function addEvent(obj, evType, fn, useCapture){ 26 | if (obj.addEventListener){ 27 | obj.addEventListener(evType, fn, useCapture); 28 | return true; 29 | } else if (obj.attachEvent){ 30 | var r = obj.attachEvent("on"+evType, fn); 31 | return r; 32 | } else { 33 | alert("Handler could not be attached"); 34 | } 35 | } 36 | 37 | function gettext(lol) { 38 | return lol; 39 | } 40 | 41 | function submit_form(type, id) { 42 | myform = $(type + '-form-' + id); 43 | new Ajax.Updater($(type + 'container' + id), myform.readAttribute('action'), { 44 | parameters: myform.serialize(true), 45 | }); 46 | } 47 | 48 | function delete_event(id) { 49 | var frm = $('calendar-form-'+id); 50 | var cnt = $('calendar-edit-'+id); 51 | new Ajax.Request('/calendar/' + id + '/delete/', { 52 | onSuccess: function(r) { 53 | var notification = document.createElement('div'); 54 | notification.className = 'notification success'; 55 | notification.innerHTML = '

    Event '+ document.getElementById('calendarcontainer' + id).getElementsByClassName('name')[0].innerHTML +' deleted!

    ' + r.responseText; 56 | document.getElementById('calendarcontainer' + id ).parentNode.parentNode.insertBefore(notification, document.getElementById('calendarcontainer' + id ).parentNode); 57 | document.getElementById('calendarcontainer' + id ).parentNode.remove(); 58 | } 59 | }) 60 | } 61 | 62 | function delete_entry(type, id) { 63 | container = $(type + 'container' + id); 64 | url = '/' + type + '/' + id + '/delete/'; 65 | new Ajax.Request(url, { 66 | onSuccess: function(response) { 67 | container.remove(); 68 | } 69 | }) 70 | } 71 | 72 | function toggleView(type, id, onoff) { 73 | view = $(type + '-view-' + id); 74 | edit = $(type + '-edit-' + id); 75 | 76 | if (onoff) { 77 | set_visible(edit); 78 | if (view) { 79 | set_invisible(view); 80 | } 81 | } else { 82 | if (view) { 83 | set_visible(view); 84 | } 85 | set_invisible(edit); 86 | } 87 | } 88 | 89 | function set_visible(obj) { 90 | obj.addClassName('visible'); 91 | obj.removeClassName('invisible'); 92 | } 93 | 94 | function set_invisible(obj){ 95 | obj.addClassName('invisible'); 96 | obj.removeClassName('visible'); 97 | } 98 | 99 | 100 | function enter_pressed(e){ 101 | var keycode; 102 | if (window.event) keycode = window.event.keyCode; 103 | else if (e) keycode = e.which; 104 | else return false; 105 | return (keycode == 13); 106 | } 107 | 108 | function submit_event(id) { 109 | var frm = $('calendar-form-'+id); 110 | var cnt = $('calendar-edit-'+id); 111 | var statusIndicator = frm.getElementsByClassName('status-indicator')[0]; 112 | statusIndicator.classList.remove('saved'); 113 | statusIndicator.innerText = 'sending...'; 114 | 115 | new Ajax.Request(frm.readAttribute('action'), { 116 | parameters: frm.serialize(true), 117 | onSuccess: function(r) { 118 | statusIndicator.innerText = 'SAVED'; 119 | statusIndicator.classList.add('saved'); 120 | window.setTimeout(function() { statusIndicator.classList.remove('saved'); }, 2000); 121 | }, 122 | onFailure: function(r) { 123 | cnt.innerHTML = r.responseText; 124 | DateTimeShortcuts.init.defer(1); 125 | } 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 3 | 4 | 5 | 6 | 7 | {% block title %}{{HOS_NAME}}{% endblock %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% block add_css %} 31 | {% endblock %} 32 | {% block add_js %} 33 | {% endblock %} 34 | 35 | 36 | 37 |
    38 |
    39 |
    40 |
    41 | 44 | 53 | 54 |
    55 | {% if not user.is_authenticated %} 56 | Login 57 | {% else %}Hi {{ user }}! 58 | {% if user.is_superuser %} 59 | | Manage 60 | | Bank Import 61 | | HETTI 62 | | Mitglieder 63 | | Announce 64 | {% endif %} 65 | |
    66 | {% csrf_token %} 67 | 68 |
    69 | {% endif %} 70 |
    71 |
    72 |
    73 | {% if messages %} 74 |
      75 | {% for message in messages %} 76 | {{ message }} 77 | {% endfor %} 78 |
    79 | {% endif %} 80 | {% block hos_content %} 81 | {% endblock %} 82 |
    83 | 84 | 88 |
    89 |
    90 |
    91 | 92 | 93 | -------------------------------------------------------------------------------- /static/stylesheets/layout.css: -------------------------------------------------------------------------------- 1 | /* ------------------------- 2 | Metalab CSS 3 | 4 | Version 1.0 5 | ---------------------------*/ 6 | 7 | #container { 8 | float: left; 9 | width: 100%; 10 | background: #fff; 11 | border: none; 12 | } 13 | 14 | #page { 15 | width: 900px; 16 | margin: 0 auto 2em auto; 17 | } 18 | 19 | /* =header */ 20 | #header { 21 | width: 100%; 22 | float: left; 23 | margin-bottom: 1em; 24 | background-color: white; 25 | } 26 | 27 | #logo { 28 | display: inline; 29 | float: left; 30 | margin-top: 0.7em; 31 | } 32 | 33 | #logo a { 34 | line-height: 1em; 35 | border: none; 36 | } 37 | 38 | #logo a:hover { 39 | background: none; 40 | } 41 | 42 | #header-nav { 43 | margin-left: 280px; 44 | } 45 | 46 | #header-nav li { 47 | float: left; 48 | } 49 | 50 | #header-nav a { 51 | float: left; 52 | color: #fff; 53 | background: #4F5C7F; 54 | margin: 0 3px 6px; 55 | padding: 0.5em 0.7em; 56 | } 57 | 58 | #header-nav a:hover { 59 | background: #7888AF; 60 | } 61 | 62 | #login { 63 | float: right; 64 | margin: 0.7em 0 0 1em; 65 | 66 | } 67 | 68 | #presence_closed{ 69 | float: right; 70 | margin-top: 0.7em; 71 | } 72 | 73 | #presence_open { 74 | float: right; 75 | margin-top: 0.7em; 76 | } 77 | 78 | /* =main */ 79 | #main { 80 | float: left; 81 | width: 100%; 82 | } 83 | 84 | #main #column_1 { 85 | width: 333px; 86 | display: inline; /* fixes IE double margin issue */ 87 | float: left; 88 | } 89 | 90 | #main #column_2 { 91 | width: 518px; 92 | float: right; 93 | } 94 | 95 | /* =project */ 96 | #project_list { 97 | float: left; 98 | width: 249px; 99 | } 100 | 101 | #recent_changes { 102 | float: right; 103 | width: 249px; 104 | } 105 | 106 | /* =calendar */ 107 | #calendar { 108 | border-bottom: 1px solid #e7e7e7; 109 | margin-bottom: 2em; 110 | padding-bottom: 1em; 111 | } 112 | 113 | /* =members */ 114 | #members { 115 | float: left; 116 | width: 100%; 117 | height: 8em; 118 | margin-bottom: 1em; 119 | padding-bottom: 1em; 120 | } 121 | 122 | #introduction{ 123 | float:left; 124 | height: 3em; 125 | border-bottom: 1px solid #e7e7e7; 126 | margin-bottom: 2em; 127 | padding-bottom: 1em; 128 | } 129 | 130 | .announcement{ 131 | float:left; 132 | margin-bottom: 2em; 133 | padding-bottom: 1em; 134 | padding: 1.5em 1em 1.5em 1.5em; 135 | background: transparent; 136 | font-weight: bold; 137 | } 138 | 139 | .announcement.red{ 140 | border-left: 4px solid red; 141 | } 142 | 143 | .announcement.green{ 144 | border-left: 4px solid green; 145 | } 146 | 147 | .announcement a{ 148 | font-weight: bold; 149 | font-size: 130%; 150 | } 151 | 152 | .announcement small{ 153 | font-weight: bold; 154 | font-size: 80%; 155 | } 156 | 157 | #extrasmall a{ 158 | font-weight: bold; 159 | font-size: 80%; 160 | } 161 | 162 | .member_pics { 163 | float: left; 164 | margin-bottom: 16px; 165 | margin-right: -5px; 166 | } 167 | 168 | .member_pics .member, .member_pics .become-member { 169 | float: left; 170 | display: block; 171 | margin: 0 5px 5px 0; 172 | width: 60px; 173 | height: 60px; 174 | box-sizing: border-box; 175 | } 176 | 177 | .member_pics .member { 178 | background-size: cover; 179 | background-position: center; 180 | border: 1px solid #ddd; 181 | line-height: 1em; 182 | } 183 | 184 | .member_pics .become-member { 185 | background: #4f5c7f; 186 | border: 1px solid #4f5c7f; 187 | color: #fff; 188 | text-align: center; 189 | font-weight: bold; 190 | line-height: 1.2; 191 | font-size: 10px; 192 | text-transform: uppercase; 193 | transition: all .3s; 194 | } 195 | .member_pics .become-member-text:before { 196 | content: "++"; 197 | font-weight: normal; 198 | display: block; 199 | font-size: 30px; 200 | line-height: 1em; 201 | color: rgba(255,255,255,.6); 202 | transition: all .3s; 203 | } 204 | .member_pics .become-member-text:hover:before { 205 | color: rgba(255,255,255,1); 206 | } 207 | 208 | 209 | 210 | .member_pics .last { 211 | margin-right: 0; 212 | } 213 | 214 | /* =footer */ 215 | #footer { 216 | clear: both; 217 | border-top: 1px solid #e7e7e7; 218 | padding: 0.5em 1em; 219 | } 220 | 221 | #content { 222 | clear: both; 223 | } 224 | 225 | #openlab { 226 | margin-bottom: 1.5em; 227 | padding-left: 0.5em; 228 | } 229 | 230 | 231 | 232 | /* small screen stuff */ 233 | @media screen and (max-width: 920px) { 234 | 235 | #page { 236 | width: 96%; 237 | margin: 0 2%; 238 | } 239 | 240 | 241 | #main #column_1, 242 | #main #column_2, 243 | #calendar-content li 244 | { 245 | width: 100% !important; 246 | box-sizing:border-box; 247 | display: block; 248 | float: none; 249 | } 250 | 251 | #project_list { 252 | width: 49%; 253 | margin-right: 2%; 254 | } 255 | 256 | #calendar, 257 | #recent_changes 258 | { 259 | width: 49%; 260 | float:left; 261 | } 262 | 263 | #openlab { 264 | padding: 0; 265 | } 266 | 267 | } 268 | 269 | 270 | /* mobile stuff */ 271 | @media screen and (max-width: 600px) { 272 | 273 | #calendar, 274 | #project_list, 275 | #recent_changes 276 | { 277 | width: 100% !important; 278 | box-sizing:border-box; 279 | display: block; 280 | float: none; 281 | } 282 | 283 | 284 | /* burger button for drop down navigation */ 285 | ul#header-nav:before { 286 | content: "\2630"; 287 | padding: 1em; 288 | background: #4F5C7F; 289 | color: #fff; 290 | position: absolute; 291 | top: 0; 292 | right: 0; 293 | width: 1em; 294 | height: 1em; 295 | line-height: 1em; 296 | font-size: 1.3em; 297 | } 298 | #header-nav { 299 | display: inline; 300 | position: absolute; 301 | z-index: 101; 302 | top: 0; 303 | padding-top: 3.8em; 304 | right: 0; 305 | margin: 0; 306 | } 307 | 308 | #header-nav > li { 309 | float: none; 310 | display:none; 311 | border-bottom: 2px solid #42495D; 312 | width: 12em; 313 | } 314 | #header-nav:hover > li, #header-nav:active > li { 315 | display: block; 316 | width: 12em; 317 | } 318 | 319 | #header-nav a { 320 | float: none; 321 | display: block; 322 | color: #fff; 323 | background: #4F5C7F; 324 | margin: 0; 325 | padding: 0.7em 1em; 326 | } 327 | 328 | /* move login link next to menu button*/ 329 | #login { 330 | margin: 1.2em 5em 0 0; 331 | } 332 | 333 | } 334 | -------------------------------------------------------------------------------- /mos/settings/common.py: -------------------------------------------------------------------------------- 1 | # Django settings for hos 2 | 3 | from unipath import FSPath as Path 4 | 5 | BASE_DIR = Path(__file__).absolute().ancestor(3) 6 | 7 | DEBUG = False 8 | TEST_RUNNER = 'django.test.runner.DiscoverRunner' 9 | 10 | ASGI_APPLICATION = "mos.routing.application" 11 | 12 | ADMINS = ( 13 | # ('Your Name', 'your_email@example.com'), 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'NAME': 'meta', 23 | 'ENGINE': 'django.db.backends.mysql', 24 | 'USER': 'mos', 25 | 'PASSWORD': '' 26 | }, 27 | } 28 | 29 | TEMPLATES = [ 30 | { 31 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 32 | 'APP_DIRS': True, 33 | 'DIRS': [ 34 | BASE_DIR.child('templates'), 35 | ], 36 | 'OPTIONS': { 37 | 'context_processors': [ 38 | "django.contrib.auth.context_processors.auth", 39 | "django.template.context_processors.debug", 40 | "django.template.context_processors.i18n", 41 | "django.template.context_processors.media", 42 | "django.template.context_processors.static", 43 | "django.template.context_processors.tz", 44 | "django.template.context_processors.request", 45 | "django.contrib.messages.context_processors.messages", 46 | 'core.context_processors.custom_settings_global', 47 | ], 48 | }, 49 | }, 50 | ] 51 | 52 | # Local timezone 53 | TIME_ZONE = 'Europe/Vienna' 54 | 55 | USE_TZ = False 56 | 57 | # Set first day of week to monday 58 | FIRST_DAY_OF_WEEK = 1 59 | 60 | # Language code for this installation. All choices can be found here: 61 | # http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes 62 | LANGUAGE_CODE = 'en-us' 63 | USE_L10N = False 64 | DATE_FORMAT = "Y-m-d" 65 | TIME_FORMAT = "H:i" 66 | DATETIME_FORMAT = f"{DATE_FORMAT} {TIME_FORMAT}" 67 | MONTH_DAY_FORMAT = "d.m" 68 | 69 | SITE_ID = 1 70 | 71 | # If you set this to False, Django will make some optimizations so as not 72 | # to load the internationalization machinery. 73 | USE_I18N = True 74 | 75 | # URL prefix for static files. 76 | # Example: "http://media.lawrence.com/static/" 77 | STATIC_URL = "/static/" 78 | 79 | # Additional locations of static files 80 | STATICFILES_DIRS = ( 81 | BASE_DIR.child("static"), 82 | ) 83 | 84 | # List of finder classes that know how to find static files in 85 | # various locations. 86 | STATICFILES_FINDERS = ( 87 | 'django.contrib.staticfiles.finders.FileSystemFinder', 88 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 89 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 90 | ) 91 | 92 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 93 | # trailing slash if there is a path component (optional in other cases). 94 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 95 | MEDIA_URL = '/media/' 96 | 97 | MIDDLEWARE = ( 98 | 'django.middleware.common.CommonMiddleware', 99 | 'django.middleware.csrf.CsrfViewMiddleware', 100 | 'django.contrib.sessions.middleware.SessionMiddleware', 101 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 102 | 'members.middleware.DeactivateUserMiddleware', 103 | 'django.contrib.messages.middleware.MessageMiddleware', 104 | 'core.middleware.SetStatFooter', # remove this row to disable 105 | # footer stats 106 | ) 107 | 108 | ROOT_URLCONF = 'mos.urls' 109 | 110 | 111 | INSTALLED_APPS = ( 112 | 'django_extensions', 113 | 'members', 114 | 'django.contrib.auth', 115 | 'django.contrib.contenttypes', 116 | 'django.contrib.sessions', 117 | 'django.contrib.sites', 118 | 'django.contrib.staticfiles', 119 | 'django.contrib.admin', 120 | 'django.contrib.admindocs', 121 | 'django.contrib.humanize', 122 | 'django.contrib.messages', 123 | 'easy_thumbnails', 124 | 'import_export', 125 | 'web', 126 | 'projects', 127 | 'cal', 128 | 'sources', 129 | 'announce', 130 | 'core', 131 | 'channels', 132 | 'things', 133 | ) 134 | 135 | 136 | LOGIN_REDIRECT_URL = '/mos' 137 | LOGIN_URL = '/member/login/' 138 | 139 | DATA_UPLOAD_MAX_NUMBER_FIELDS=3000 140 | 141 | #--- Custom Options ---------------------------------------------------------- 142 | HOS_URL_PREFIX = '/' 143 | HOS_NAME = 'Metalab OS' 144 | HOS_HOME_EVENT_NUM = 5 145 | HOS_WIKI_URL = '/wiki/' 146 | HOS_WIKI_FULL_URL = 'https://metalab.at/wiki/' 147 | MEDIAWIKI_API = HOS_WIKI_FULL_URL + "api.php" 148 | HOS_ANNOUNCE_FROM = 'core@metalab.at' 149 | HOS_SEPA_CREDITOR_ID = 'AT12ZZZ00000000001' 150 | HOS_SEPA_CREDITOR_NAME = 'Verein Metalab' 151 | HOS_SEPA_CREDITOR_IBAN = 'AT483200000012345864' 152 | HOS_SEPA_CREDITOR_BIC = 'GIBAATWWXXX' 153 | HOS_SEPA_SCHEMA = 'pain.008.001.02' 154 | HOS_SEPA_CURRENCY = 'EUR' # ISO 4217 155 | HOS_SEPA_BATCH = True 156 | 157 | HOS_ANNOUNCE_LOG = '../announce.log' 158 | HOS_EMAIL_LOG = '../email.log' 159 | 160 | MOS_WIKI_CHANGE_URL = 'https://metalab.at/wiki/index.php?title=Spezial:Letzte_%C3%84nderungen&feed=atom' 161 | MOS_WIKI_KEEP = 5 162 | 163 | # ----------------- Style --------------------- 164 | HOS_CUSTOM_STYLE = '' # name of the custom style, blank for default 165 | HOS_MEMBER_GALLERY = True 166 | HOS_CALENDAR = True 167 | HOS_OPENLAB = True 168 | HOS_INTRODUCTION = True 169 | HOS_PROJECTS = True 170 | HOS_RECENT_CHANGES = True 171 | 172 | # ----------------- Jour Fixe Reminder ------------ 173 | MOS_JF_DAYS_IN_ADVANCE = 3 174 | MOS_JF_DB_ID = 2 # id of events of type "Jour Fixe" in the database 175 | MOS_JF_SENDER = 'jf-reminder@metalab.at' 176 | MOS_JF_RECIPIENTS = ['intern@lists.metalab.at'] 177 | 178 | # ----------------- Thumbnail Settings ------------ 179 | THUMBNAIL_ALIASES = { 180 | '': { 181 | 'avatar': {'size': (120, 120), 'crop': False}, 182 | }, 183 | } 184 | 185 | # ------------- Matrix Intern Inviter ------------- 186 | MATRIX_USERNAME = "@metalab_room_inviter_bot:matrix.org" 187 | MATRIX_PASSWORD = None # Is setup in environment variables 188 | MATRIX_ROOM_NAME = "#metalab-intern:matrix.org" 189 | MATRIX_ROOM_ID = None # Is setup in environment variables 190 | --------------------------------------------------------------------------------