GithubHelp home page GithubHelp logo

ocf / ocflib Goto Github PK

View Code? Open in Web Editor NEW
15.0 45.0 33.0 1.45 MB

Python libraries for account and server management

Home Page: https://pypi.python.org/pypi/ocflib

License: Other

Makefile 0.54% Python 97.95% Shell 0.17% Jinja 0.64% Nix 0.69%
python pypi python-library ocf jenkinsfile pre-commit makefile debian-packaging

ocflib's Introduction

ocflib

Build Status Coverage Status PyPI version

ocflib is a Python library for working with Open Computing Facility services (in particular, accounts and server management).

The library targets Python 3.5.3 and 3.7 (the versions available in Debian stretch and buster).

The goal of the library is to make it easier to re-use OCF python code. In the past, code was split between approve, atool, create, chpass, sorry, signat, etc., which made it difficult to do things like share common password requirements.

What belongs here

In general, code which can be re-used should be here, but standalone applications or binaries shouldn't. For example, ocfweb uses ocflib code to change passwords and create accounts, but the Django web app doesn't belong here.

Using on OCF

ocflib is installed by Puppet on the OCF, so you can simply do things like import ocflib.lab.stats from the system python3 installation. We don't install it to python2 site-packages.

We build a Debian package which is installed by Puppet. We also publish new versions to PyPI, which is useful because it allows easy installation into virtualenvs.

Note about lockfiles

This repository includes a poetry.lock file. Lockfiles are usually used to ensure that the exact same versions of dependencies are installed across different machines. However, as this is a library, we don't want to force downstream users to use the exact same versions of dependencies as us, and indeed, the lockfile is ignored when distributing. We still include it in the repository to make it easier to develop, test, and debug ocflib.

Installing locally

For Testing Changes

Development of ocflib uses Poetry. The easiest way to test changes to ocflib is to let Poetry manage the virtual environment for you:

poetry install
poetry shell

Now, if you import something from ocflib, you'll be using the version from your working copy.

Testing and linting

We use pytest to test our code, and pre-commit to lint it. You should run make test before pushing to run both.

The tests directory contains automated tests which you're encouraged to add to (and not break). The tests-manual directory contains scripts intended for testing.

Using pre-commit

We use pre-commit to lint our code before commiting. While some of the rules might seem a little arbitrary, it helps keep the style consistent, and ensure annoying things like trailing whitespace don't creep in.

You can simply run make install-hooks to install the necessary git hooks; once installed, pre-commit will run every time you commit.

Alternatively, if you'd rather not install any hooks, you can simply use make test as usual, which will also run the hooks.

Troubleshooting: Cracklib Error

If you're trying to run make install-hooks on ocfweb (or related repos) and get this error:

./_cracklib.c:40:10: fatal error: 'crack.h' file not found
  #include <crack.h>
           ^~~~~~~~~
  1 error generated.

The issue relates to the cracklib package not finding the necessary header files to install. Make sure cracklib is installed on your machine (https://github.com/cracklib/cracklib, if you're on Mac, brew install cracklib).

Deploying changes

Deploying changes involves:

  • Running tests and linters
  • Pushing a new version to PyPI
  • Building a Debian package
  • Pushing the Debian package to our internal apt

Jenkins will automatically perform all of these steps for you on every push, including automatically generating a new version number. As long as make test passes, your code will be automatically deployed. You can monitor the progress of your deploy here.

ocflib's People

Contributors

abizer avatar asaiacai avatar baisang avatar ben9583 avatar bjb568 avatar boomaa23 avatar bzh-bzh avatar cg505 avatar chriskuehl avatar daradib avatar dkess avatar douglaslwong avatar ethanhs avatar fydai avatar ja5087 avatar jvperrin avatar kalissaac avatar kkuehlz avatar kpengboy avatar matthew-mcallister avatar mcer4294967296 avatar mrminos avatar nickimpicciche avatar nikhiljha avatar oliver-ni avatar sahilhasan avatar shex1627 avatar singingtelegram avatar tahabi avatar tliu22 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ocflib's Issues

similarity_heuristic is bad

The code smells and it has a ton of false positives and false negatives. I couldn't even describe to you what the function does without specifying the names of the multiple algorithms involved as well as the order and limits with which it processes match attempts. It would better off returning random.choice([0, 3]). This isn't to say that there's a better algorithm that can be used or that the code ought to be cleaned up, though those are probably both true statements, but the fundamental problem with requiring a similar username and real name is that not only is that similarity subjective, but nobody even knows what it means. That makes this not just a code smell, but a UX smell.

  • If you want to be real draconian about having usernames be real names, generate the usernames programmatically and don't let the user change it without contacting humans (to deal with the edge case that your programatic solution makes their name sound offensive or something).
  • Probably the best thing to do is remove the check and allow some freedom.

Error in check script

I ran check ith, and got the following error:

bernardzhao@invasion:~$ check ith
\ith:*:60147:1000:Thee Ho:/home/i/it/ith:/bin/bash
Created on: 2019-05-01
Member of group(s): ocf ocfstaff
CalNet UID number: 1662297

ith printing quota:
  → 196 remaining this semester
  → 20 remaining today

Signatories/Signatory for:
Traceback (most recent call last):
  File "/opt/share/utils/sbin/signat", line 212, in <module>
    main()
  File "/opt/share/utils/sbin/signat", line 204, in main
    show_by_username(args.username)
  File "/opt/share/utils/sbin/signat", line 49, in show_by_username
    show_groups_by_student_signat(uid)
  File "/opt/share/utils/sbin/signat", line 88, in show_groups_by_student_signat
    name = directory.name_by_calnet_uid(uid)
  File "/usr/lib/python3/dist-packages/ocflib/ucb/directory.py", line 20, in name_by_calnet_uid
    names = get_calnet_names(uid)
  File "/usr/lib/python3/dist-packages/ocflib/ucb/directory.py", line 9, in get_calnet_names
    attrs = search.user_attrs_ucb(uid)
  File "/usr/lib/python3/dist-packages/ocflib/account/search.py", line 65, in user_attrs_ucb
    base=UCB_LDAP_PEOPLE)
  File "/usr/lib/python3/dist-packages/ocflib/account/search.py", line 55, in user_attrs
    '(uid={})'.format(escape_filter_chars(uid)),
  File "/usr/lib/python3/dist-packages/ldap3/utils/conv.py", line 36, in escape_filter_chars
    output = text.replace('\\', r'\5c')
AttributeError: 'list' object has no attribute 'replace'

Retry connection to callink API after read timeout

It seems that callink will (not infrequently, but rarely enough...) timeout on trying to connect to their API endpoint. We should retry the connection. The error looks something like:

.... more traceback above

Traceback (most recent call last):
  File "/opt/share/utils/sbin/signat", line 212, in <module>
    main()
  File "/opt/share/utils/sbin/signat", line 204, in main
    show_by_username(args.username)
  File "/opt/share/utils/sbin/signat", line 49, in show_by_username
    show_groups_by_student_signat(uid)
  File "/opt/share/utils/sbin/signat", line 97, in show_groups_by_student_signat
    groups = groups_by_student_signat(uid)
  File "/usr/lib/python3/dist-packages/ocflib/ucb/groups.py", line 135, in groups_by_student_signat
    return _get_osl({'UID': uid}, service, parser)
  File "/usr/lib/python3/dist-packages/ocflib/ucb/groups.py", line 150, in _get_osl
    r = requests.get(url, timeout=20)
  File "/usr/lib/python3/dist-packages/requests/api.py", line 75, in get
    return request('get', url, params=params, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/api.py", line 60, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 533, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 646, in send
    r = adapter.send(request, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/adapters.py", line 529, in send
    raise ReadTimeout(e, request=request)
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='studentgroupservice.sait-west.berkeley.edu', port=443): Readtimed out. (read timeout=20)

ocflib.account.search.users_by_filter can KeyError

Traceback (most recent call last):
  File "staff/acct/guess-passwords", line 47, in <module>
    exit(main())
  File "staff/acct/guess-passwords", line 36, in main
    usernames = unsorried_users()
  File "staff/acct/guess-passwords", line 18, in unsorried_users
    return users_by_filter('(!(loginShell={}))'.format(SORRIED_SHELL))
  File "/usr/lib/python3/dist-packages/ocflib/account/search.py", line 12, in users_by_filter
    return [entry['attributes']['uid'][0] for entry in c.response]
  File "/usr/lib/python3/dist-packages/ocflib/account/search.py", line 12, in <listcomp>
    return [entry['attributes']['uid'][0] for entry in c.response]
KeyError: 'uid'

Debugging:

(Pdb) p len(c.response)
24679
(Pdb) derp = [user for user in c.response if 'uid' not in user['attributes']]                                    
(Pdb) p derp
[{'type': 'searchResEntry', 'raw_attributes': {}, 'attributes': {}, 'dn': 'ou=People,dc=OCF,dc=Berkeley,dc=EDU'}]

Add duration information to tests

Having duration information like in ocfweb would be nice, however currently it doesn't work because of freezegun. I think some more debugging is needed to get the two working well together (or maybe file an issue/PR on either pytest or freezegun). The current output is something like this, which is obviously false:

============================================= slowest 10 test durations ==============================================
71063314.99s teardown tests/lab/hours_test.py::TestDay::test_creation[None-Saturday-None-hours5]
71063314.92s teardown tests/lab/hours_test.py::TestDay::test_creation[when4-Sunday-Random 3 Days-hours4]
71063314.84s teardown tests/lab/hours_test.py::TestDay::test_creation[when3-Saturday-Pi Day-hours3]
71063314.77s teardown tests/lab/hours_test.py::TestDay::test_creation[when2-Wednesday-None-hours2]
71063314.70s teardown tests/lab/hours_test.py::TestDay::test_creation[when1-Sunday-None-hours1]
71063314.63s teardown tests/lab/hours_test.py::TestDay::test_creation[when0-Sunday-None-hours0]
71063314.51s teardown tests/lab/hours_test.py::test_is_open[now12-False]
71063314.43s teardown tests/lab/hours_test.py::test_is_open[now11-True]
71063314.36s teardown tests/lab/hours_test.py::test_is_open[now10-True]
71063314.29s teardown tests/lab/hours_test.py::test_is_open[now9-True]

A possible workaround I suppose would be to make the time used in the freezegun mock something long in the future so that the time is then negative? Doesn't seem great though :(

Deprecate functions that use anonymous MySQL user

We currently have an anonymous MySQL user, which is used in three places:

Having a globally accessible MySQL user is very bad for security, so we should investigate ways to change how these modules work to not rely on a user.

If a function is only used internally, we should put it behind a MySQL account with a password. If it needs to be accessible globally (I don't think any of these do), we would need to make an ocfweb API for it.

Issues with DB test setup

Apparently it yielded the socket before the DB is ready... I added a 5s sleep but it's only a temporary hack

Remove colorama dep

We don't really need it, we only use it for the color code constants. It does a lot of other stuff (e.g. wrapping stdout for Windows support) which we don't need.

change dnspython3 => dnspython

Running setup.py install for dnspython3
  ==============================================================================
  dnspython3 is now superseded by the regular dnspython kit, which
  now supports Python 2 and Python 3.  Installing dnspython as a dependency.
  ==============================================================================

easy change in setup.py, just need to make sure the resulting debian package is still built correctly (it should still depend on python3-dnspython

Fix date-dependent testcases

Some testcases in tests/printing/quota_tests.py don't work properly on the first day of each semester.

IRC discussion:

<jvperrin-slack> Any idea why printing quotas aren't changing at all?
<jvperrin-slack> Seems like it's related to it being a new year, but I can't see why that would matter
<jvperrin-slack> Ah, I think I found it
<jvperrin-slack> It's a greater when there should be a greater than or equal to
<jvperrin-slack> https://github.com/ocf/ocflib/blob/master/ocflib/printing/ocfprinting.sql#L85
<kochira> Web Page Title: ocflib/ocfprinting.sql at master · ocf/ocflib · GitHub
<jvperrin-slack> and https://github.com/ocf/ocflib/blob/master/ocflib/printing/ocfprinting.sql#L92
<kochira> Web Page Title: ocflib/ocfprinting.sql at master · ocf/ocflib · GitHub
<jvperrin-slack> So that means nothing done on the first day of the year (or approximately the first of August) has any effect on quotas lol
<jvperrin-slack> Unofficial free printing days :P
<jvperrin-slack> This really doesn't work when yesterday is in a different year lol: https://github.com/ocf/ocflib/blob/master/tests/printing/quota_test.py#L181
<kochira> Web Page Title: ocflib/quota_test.py at master · ocf/ocflib · GitHub
<jvperrin-slack> This is great
<ckuehl> lol these are my favorite kind of test failures
<kpengboy> Do we need the test to test today or can we just fix a date
<jvperrin-slack> Uh, it can probably just fix a date?
<jvperrin-slack> The issue with fixing a date is that it uses the database table that still uses the actual date for the current day's quota, so that doesn't exactly work
<ckuehl> yea our other tests use freezegun for that
<ckuehl> but not sure how you'd do that given these actually reach out to a mysql
<ckuehl> at least one of the tests also fails if it hits midnight in the middle of the test
<ckuehl> lol
<jvperrin-slack> Yup, I was thinking about that one too
<jvperrin-slack> Hmm :/
<jvperrin-slack> Well only 45 minutes left to fix it :P
<ckuehl> lol yea
<ckuehl> it's a true "wait for it to fix itself" bug
<jvperrin-slack> It'll break again someday in August though
<jvperrin-slack> And if the day changes in the middle of a test I suppose
<jvperrin-slack> Not sure how you really make this a reproducible test though, fake the time in mysql or something?
<jvperrin-slack> Hmm: http://dba.stackexchange.com/questions/81305/how-do-i-unit-test-queries-which-are-based-on-dates-from-now
<kochira> Web Page Title: mysql - How do I unit test queries which are based on dates from now? - Database Administrators Stack Exchange

Improve database connection API to actually close opened connections

Taken from @chriskuehl's comment on #103:

This is fine, but eventually we should take the opportunity to fix this API. Currently it's really hard to write correct code which closes connections as you'd expect; a better API would be something like this:

@contextlib.contextmanager
def get_connection(...):
    with contextlib.closing(pymysql.connect(...)) as conn:
        yield conn

Then usage is like this:

with get_connection() as conn:
    with conn as cursor:
        cursor.execute('...')

Currently we never call close on the connections we open.

Investigate text encoding for passwords

Interesting rootspam from last night:

 An exception occured in ocfweb:

Traceback (most recent call last):
  File "/opt/ocfweb/venv/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/ocfweb/ocfweb/auth.py", line 53, in wrapper
    return fn(request, *args, **kwargs)
  File "/opt/ocfweb/ocfweb/account/register.py", line 75, in request_account
    RSA.importKey(CREATE_PUBLIC_KEY),
  File "/opt/ocfweb/venv/lib/python3.7/site-packages/ocflib/account/creation.py", line 435, in encrypt_password
    return RSA_CIPHER.encrypt(password.encode('ascii'))
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)

Request:
  * Host: www.ocf.berkeley.edu
  * Path: /account/register/
  * Method: POST
  * Secure: True

I assume this is from someone trying to use non-ascii character in their password. I am not sure what the best practices for this are, but we should investigate this further and see if we can avoid using the ascii encoding.

LDAP search (ocflib.account.search) doesn't escape searches properly

This causes errors like this, when a search contains parentheses:

Traceback (most recent call last):
  File "/opt/ocfweb/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/ocfweb/ocfweb/account/register.py", line 122, in recommend
    recommendations = recommender.recommend(real_name, 10)
  File "/opt/ocfweb/ocfweb/account/recommender.py", line 30, in recommend
    validate_username(rec, real_name)
  File "/opt/ocfweb/venv/lib/python3.5/site-packages/ocflib/account/creation.py", line 328, in validate_username
    if search.user_exists(username):
  File "/opt/ocfweb/venv/lib/python3.5/site-packages/ocflib/account/search.py", line 61, in user_exists
    return bool(user_attrs(account))
  File "/opt/ocfweb/venv/lib/python3.5/site-packages/ocflib/account/search.py", line 48, in user_attrs
    c.search(base, '(uid={})'.format(uid), attributes=ldap3.ALL_ATTRIBUTES)
  File "/opt/ocfweb/venv/lib/python3.5/site-packages/ldap3/core/connection.py", line 779, in search
    check_names=self.check_names)
  File "/opt/ocfweb/venv/lib/python3.5/site-packages/ldap3/operation/search.py", line 372, in search_operation
    request['filter'] = compile_filter(parse_filter(search_filter, schema, auto_escape, auto_encode, validator, check_names).elements[0])  # parse the searchFilter string and compile it starting from the root node
  File "/opt/ocfweb/venv/lib/python3.5/site-packages/ldap3/operation/search.py", line 220, in parse_filter
    raise LDAPInvalidFilterError('invalid filter')
ldap3.core.exceptions.LDAPInvalidFilterError: invalid filter



Request:
  * Host: www.ocf.berkeley.edu
  * Path: /account/register/recommend/?real_name=Xiao%20(Calvin)%20Hu
  * Method: GET
  * Secure: True

We might be able to use escape_bytes from ldap3 or maybe just escape parentheses and move on since we quite rarely encounter this issue.

upgrade pexpect past 3.2

in setup.py:

        # pexpect 3.3 (currently the latest) breaks with multiprocessing/celery
        # https://github.com/pexpect/pexpect/issues/86
        'pexpect<3.2.999',

looks like we're up to pexpect 4.0.1, so we can probably unpin that, just need some small amount of testing

Reserve UIDs in 60000 range

There are some UIDs that we better not hand out to new users:

  • 65534 is the nobody user
  • 65535 is sometimes used as an invalid user
  • 61184-65519 is reserved by systemd for dynamic users

create throws an uncaught exception when you reject a username that doesn't exist

A problem was encountered and reported via ocflib:

An exception occured in create:

Traceback (most recent call last):
  File "/opt/create/venv/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 1710, in delete
    state = attributes.instance_state(instance)
AttributeError: 'NoneType' object has no attribute '_sa_instance_state'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/create/venv/lib/python3.5/site-packages/celery/app/trace.py", line 367, in trace_task
    R = retval = fun(*args, **kwargs)
  File "/opt/create/venv/lib/python3.5/site-packages/celery/app/trace.py", line 622, in __protected_call__
    return self.run(*args, **kwargs)
  File "/opt/create/venv/lib/python3.5/site-packages/ocflib/account/submission.py", line 304, in reject_request
    stored_request = get_remove_row_by_user_name(user_name)
  File "/opt/create/venv/lib/python3.5/site-packages/ocflib/account/submission.py", line 292, in get_remove_row_by_user_name
    session.delete(request_row)
  File "/opt/create/venv/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 1712, in delete
    raise exc.UnmappedInstanceError(instance)
sqlalchemy.orm.exc.UnmappedInstanceError: Class 'builtins.NoneType' is not mapped


Task Details:
  * task_id: 9853248b-ee73-49c2-902c-0199d59033ba

Try `journalctl -u ocf-create` for more details.

====
Hostname: 41424c6fc0e1
Callstack:
    at /opt/create/venv/lib/python3.5/site-packages/ocflib/misc/mail.py:94 (send_problem_report)
        by /opt/create/create/tasks.py:88 (failure_handler)
        by /opt/create/venv/lib/python3.5/site-packages/celery/app/trace.py:205 (handle_failure)
        by /opt/create/venv/lib/python3.5/site-packages/celery/app/trace.py:157 (handle_error_state)
        by /opt/create/venv/lib/python3.5/site-packages/celery/app/trace.py:323 (on_error)
        by /opt/create/venv/lib/python3.5/site-packages/celery/app/trace.py:381 (trace_task)
        by /opt/create/venv/lib/python3.5/site-packages/celery/app/trace.py:530 (_fast_trace_task)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/pool.py:359 (workloop)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/pool.py:290 (__call__)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/process.py:112 (run)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/process.py:306 (_bootstrap)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/popen_fork.py:79 (_launch)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/popen_fork.py:24 (__init__)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/context.py:333 (_Popen)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/process.py:122 (start)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/pool.py:1117 (_create_worker_process)
        by /opt/create/venv/lib/python3.5/site-packages/celery/concurrency/asynpool.py:439 (_create_worker_process)
        by /opt/create/venv/lib/python3.5/site-packages/billiard/pool.py:1008 (__init__)
        by /opt/create/venv/lib/python3.5/site-packages/celery/concurrency/asynpool.py:422 (__init__)
        by /opt/create/venv/lib/python3.5/site-packages/celery/concurrency/prefork.py:112 (on_start)
        by /opt/create/venv/lib/python3.5/site-packages/celery/concurrency/base.py:131 (start)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bootsteps.py:370 (start)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bootsteps.py:119 (start)
        by /opt/create/venv/lib/python3.5/site-packages/celery/worker/worker.py:203 (start)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/worker.py:256 (run)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/base.py:244 (__call__)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/worker.py:221 (run_from_argv)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/celery.py:412 (execute)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/celery.py:480 (handle_argv)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/base.py:281 (execute_from_commandline)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/celery.py:488 (execute_from_commandline)
        by /opt/create/venv/lib/python3.5/site-packages/celery/bin/celery.py:326 (main)
        by /opt/create/venv/lib/python3.5/site-packages/celery/__main__.py:14 (main)
        by /opt/create/venv/bin/celery:11 (<module>)

use LDAP privileged bind in account creation

in #140 we had to remove verification against the berkeleyEduAffiliation LDAP attribute because anonymous access was going away. Now, we've received a privileged bind to access the attribute again, so we should re-add support for verifying member affiliations with the privileged bind.

Update meeting-hours API to support edge cases

Looking at the potential meeting times for this semester, it's looking like Web and DDR committees may meet at the same time and Finance is meeting every other week, neither of which are supported by our current meeting-hours API. Even if the former doesn't happen, it's still good to make sure we can handle this for the future.

For conflicting meeting times, we'll probably just have /api/meetings/current and /api/meetings/next return a Listpopulated by all the same date-and-time meetings instead of returning a single meeting. Should be no need to change /api/meetings/list.

As for biweekly meetings (or n-weekly meetings, for that matter), not sure how we'll keep track of off-and-on weeks, especially considering school breaks, but we'll probably also have to update schemas/meeting_hours.json and configs/meeting_hours.yaml in etc.

Anyway, I'll probably work on this myself and make a pr in both repos when that's figured out.

Relevant etc issue: ocf/etc#344

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.